pax_global_header00006660000000000000000000000064147660014520014517gustar00rootroot0000000000000052 comment=68f5eab0b0fa08becebbed412947ba19246c2518 foot-1.21.0/000077500000000000000000000000001476600145200125475ustar00rootroot00000000000000foot-1.21.0/.builds/000077500000000000000000000000001476600145200141075ustar00rootroot00000000000000foot-1.21.0/.builds/alpine-x64.yml.disabled000066400000000000000000000026241476600145200202730ustar00rootroot00000000000000image: alpine/edge packages: - musl-dev - eudev-libs - eudev-dev - linux-headers - meson - ninja - gcc - scdoc - wayland-dev - wayland-protocols - freetype-dev - fontconfig-dev - harfbuzz-dev - utf8proc-dev - pixman-dev - libxkbcommon-dev - ncurses - python3 - py3-pip - check-dev - ttf-hack - font-noto-emoji sources: - https://git.sr.ht/~dnkl/foot # triggers: # - action: email # condition: failure # to: tasks: - fcft: | cd foot/subprojects git clone https://codeberg.org/dnkl/fcft.git cd ../.. - debug: | mkdir -p bld/debug meson --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug ninja -C bld/debug -k0 meson test -C bld/debug --print-errorlogs - release: | mkdir -p bld/release meson --buildtype=minsize -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release ninja -C bld/release -k0 meson test -C bld/release --print-errorlogs - codespell: | python3 -m venv codespell-venv source codespell-venv/bin/activate pip install codespell cd foot ~/.local/bin/codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd deactivate foot-1.21.0/.builds/alpine-x86.yml.disabled000066400000000000000000000020441476600145200202730ustar00rootroot00000000000000image: alpine/edge arch: x86 packages: - musl-dev - eudev-libs - eudev-dev - linux-headers - meson - ninja - gcc - scdoc - wayland-dev - wayland-protocols - freetype-dev - fontconfig-dev - harfbuzz-dev - utf8proc-dev - pixman-dev - libxkbcommon-dev - ncurses - check-dev - ttf-hack - font-noto-emoji sources: - https://git.sr.ht/~dnkl/foot # triggers: # - action: email # condition: failure # to: tasks: - debug: | mkdir -p bld/debug meson --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug ninja -C bld/debug -k0 meson test -C bld/debug --print-errorlogs - release: | mkdir -p bld/release meson --buildtype=minsize -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release ninja -C bld/release -k0 meson test -C bld/release --print-errorlogs foot-1.21.0/.builds/freebsd-x64.yml000066400000000000000000000024731476600145200166710ustar00rootroot00000000000000image: freebsd/latest packages: - evdev-proto - libepoll-shim - meson - ninja - pkgconf - scdoc - wayland - wayland-protocols - freetype2 - fontconfig - harfbuzz - utf8proc - pixman - libxkbcommon - check - hack-font - noto-emoji sources: - https://codeberg.org/dnkl/foot.git # triggers: # - action: email # condition: failure # to: tasks: - fcft: | cd foot/subprojects git clone https://codeberg.org/dnkl/tllist.git git clone https://codeberg.org/dnkl/fcft.git cd ../.. - debug: | mkdir -p bld/debug meson setup --buildtype=debug -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug ninja -C bld/debug -k0 meson test -C bld/debug --print-errorlogs bld/debug/foot --version bld/debug/footclient --version - release: | mkdir -p bld/release meson setup --buildtype=minsize -Db_pgo=generate -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release ninja -C bld/release -k0 meson test -C bld/release --print-errorlogs bld/release/foot --version bld/release/footclient --version foot-1.21.0/.editorconfig000066400000000000000000000004171476600145200152260ustar00rootroot00000000000000root = true [*] end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true charset = utf-8 indent_style = space indent_size = 4 max_line_length = 70 [{meson.build,PKGBUILD}] indent_size = 2 [*.scd] indent_style = tab trim_trailing_whitespace = false foot-1.21.0/.forgejo/000077500000000000000000000000001476600145200142605ustar00rootroot00000000000000foot-1.21.0/.forgejo/issue_template/000077500000000000000000000000001476600145200173035ustar00rootroot00000000000000foot-1.21.0/.forgejo/issue_template/bug.yml000066400000000000000000000103571476600145200206110ustar00rootroot00000000000000name: 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 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 foot-1.21.0/.forgejo/issue_template/config.yml000066400000000000000000000002621476600145200212730ustar00rootroot00000000000000blank_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 foot-1.21.0/.forgejo/issue_template/feature_request.yml000066400000000000000000000015311476600145200232310ustar00rootroot00000000000000name: 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 foot-1.21.0/.gitignore000066400000000000000000000000421476600145200145330ustar00rootroot00000000000000/bld/ /pkg/ /src/ /subprojects/*/ foot-1.21.0/.gitmodules000066400000000000000000000000001476600145200147120ustar00rootroot00000000000000foot-1.21.0/.woodpecker.yaml000066400000000000000000000107011476600145200156520ustar00rootroot00000000000000# -*- yaml -*- steps: - name: codespell when: - event: [manual, pull_request] - event: [push, tag] branch: [master, releases/*] image: alpine:edge commands: - apk add openssl - apk add python3 - apk add py3-pip - python3 -m venv codespell-venv - source codespell-venv/bin/activate - pip install codespell - codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd - deactivate - name: subprojects when: - event: [manual, pull_request] - event: [push, tag] branch: [master, releases/*] image: alpine:edge commands: - apk add git - mkdir -p subprojects && cd subprojects - git clone https://codeberg.org/dnkl/tllist.git - git clone https://codeberg.org/dnkl/fcft.git - cd .. - name: x64 when: - event: [manual, pull_request] - event: [push, tag] branch: [master, releases/*] depends_on: [subprojects] image: alpine:edge commands: - apk update - apk add musl-dev linux-headers meson ninja gcc clang scdoc ncurses - apk add libxkbcommon-dev pixman-dev freetype-dev fontconfig-dev harfbuzz-dev utf8proc-dev - apk add wayland-dev wayland-protocols - apk add git - apk add check-dev - apk add ttf-hack font-noto-emoji # Debug - mkdir -p bld/debug-x64 - cd bld/debug-x64 - meson setup --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version - ./footclient --version - cd ../.. # Release (gcc) - mkdir -p bld/release-x64 - cd bld/release-x64 - meson setup --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version - ./footclient --version - cd ../.. # Release (clang) - mkdir -p bld/release-x64-clang - cd bld/release-x64-clang - CC=clang meson setup --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version - ./footclient --version - cd ../.. # no grapheme clustering - apk del harfbuzz harfbuzz-dev utf8proc utf8proc-dev - mkdir -p bld/debug - cd bld/debug - meson setup --buildtype=debug -Dgrapheme-clustering=disabled -Dfcft:grapheme-shaping=disabled -Dfcft:run-shaping=disabled -Dfcft:test-text-shaping=false ../.. - ninja -v -k0 - ninja -v test - ./foot --version - ./footclient --version - cd ../.. - name: x86 when: - event: [manual, pull_request] - event: [push, tag] branch: [master, releases/*] depends_on: [subprojects] image: i386/alpine:edge commands: - apk update - apk add musl-dev linux-headers meson ninja gcc clang scdoc ncurses - apk add libxkbcommon-dev pixman-dev freetype-dev fontconfig-dev harfbuzz-dev utf8proc-dev - apk add wayland-dev wayland-protocols - apk add git - apk add check-dev - apk add ttf-hack font-noto-emoji # Debug - mkdir -p bld/debug-x86 - cd bld/debug-x86 - meson setup --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version - ./footclient --version - cd ../.. # Release (gcc) - mkdir -p bld/release-x86 - cd bld/release-x86 - meson setup --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version - ./footclient --version - cd ../.. # Release (clang) - mkdir -p bld/release-x86-clang - cd bld/release-x86-clang - CC=clang meson setup --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. - ninja -v -k0 - ninja -v test - ./foot --version - ./footclient --version - cd ../.. foot-1.21.0/CHANGELOG.md000066400000000000000000003544231476600145200143730ustar00rootroot00000000000000# Changelog * [1.21.0](#1-21-0) * [1.20.2](#1-20-2) * [1.20.1](#1-20-1) * [1.20.0](#1-20-0) * [1.19.0](#1-19-0) * [1.18.1](#1-18-1) * [1.18.0](#1-18-0) * [1.17.2](#1-17-2) * [1.17.1](#1-17-1) * [1.17.0](#1-17-0) * [1.16.2](#1-16-2) * [1.16.1](#1-16-1) * [1.16.0](#1-16-0) * [1.15.3](#1-15-3) * [1.15.2](#1-15-2) * [1.15.1](#1-15-1) * [1.15.0](#1-15-0) * [1.14.0](#1-14-0) * [1.13.1](#1-13-1) * [1.13.0](#1-13-0) * [1.12.1](#1-12-1) * [1.12.0](#1-12-0) * [1.11.0](#1-11-0) * [1.10.3](#1-10-3) * [1.10.2](#1-10-2) * [1.10.1](#1-10-1) * [1.10.0](#1-10-0) * [1.9.2](#1-9-2) * [1.9.1](#1-9-1) * [1.9.0](#1-9-0) * [1.8.2](#1-8-2) * [1.8.1](#1-8-1) * [1.8.0](#1-8-0) * [1.7.2](#1-7-2) * [1.7.1](#1-7-1) * [1.7.0](#1-7-0) * [1.6.4](#1-6-4) * [1.6.3](#1-6-3) * [1.6.2](#1-6-2) * [1.6.1](#1-6-1) * [1.6.0](#1-6-0) * [1.5.4](#1-5-4) * [1.5.3](#1-5-3) * [1.5.2](#1-5-2) * [1.5.1](#1-5-1) * [1.5.0](#1-5-0) * [1.4.4](#1-4-4) * [1.4.3](#1-4-3) * [1.4.2](#1-4-2) * [1.4.1](#1-4-1) * [1.4.0](#1-4-0) * [1.3.0](#1-3-0) * [1.2.3](#1-2-3) * [1.2.2](#1-2-2) * [1.2.1](#1-2-1) * [1.2.0](#1-2-0) ## 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]). * `--server=` not working on FreeBSD ([#1956][1956]). * Crash when resetting the terminal and an application had previously set a custom app ID ([#1963][1963]) * Grapheme clustering state not reset on cursor movements. * Kitty keyboard protocol: no release events emitted for composed keys. * IME: the initial cursor position was reported as 0,0,0,0 ([#1994][1994]). [1918]: https://codeberg.org/dnkl/foot/issues/1918 [1929]: https://codeberg.org/dnkl/foot/issues/1929 [1947]: https://codeberg.org/dnkl/foot/issues/1947 [1954]: https://codeberg.org/dnkl/foot/issues/1954 [1960]: https://codeberg.org/dnkl/foot/issues/1960 [1956]: https://codeberg.org/dnkl/foot/issues/1956 [1963]: https://codeberg.org/dnkl/foot/issues/1963 [1994]: https://codeberg.org/dnkl/foot/issues/1994 ### Contributors * Adrian fxj9a * Alexander Orzechowski * Attila Fidan * camel-cdr * Craig Barnes * Guillaume Outters * Johannes Altmanninger * Ludovico Gerardi * sewn * Thomas Bonnefille ## 1.20.2 ### Changed * The `CSI 21 t` (report window title) and `OSC 176 ?` (report app-id) escape sequences are now ignored ([#1894][1894]). [1894]: https://codeberg.org/dnkl/foot/issues/1894 ### Fixed * 'flash' overlay (triggered by either `tput flash`, or enabling `bell.visual` and then sending `BEL` to the terminal) stuck when `colors.flash-alpha=1.0`. * Crash when compositor sends a keyboard enter event before the foot window has been mapped ([#1910][1910]). * Build failures (`utf8proc.h` not found) on at least FreeBSD, but most likely other BSDs, as well as some Linuxes ([#1903][1903]). [1910]: https://codeberg.org/dnkl/foot/issues/1910 [1903]: https://codeberg.org/dnkl/foot/issues/1903 ### Contributors * Alexander Orzechowski ## 1.20.1 ### Changed * Runtime changes to the app-id (OSC-176) now limits the app-id string to 2048 characters ([#1897][1897]). * `colors.flash-alpha` can no longer be set to 1.0 (i.e. fully opaque). This fixes an issue where the window would be stuck in the flash state. [1897]: https://codeberg.org/dnkl/foot/issues/1897 ### Fixed * Regression: trying to print a Unicode _"Legacy Computing symbol"_, in the range U+1FB00 - U+1FB9B would crash foot ([#1901][1901]). [1901]: https://codeberg.org/dnkl/foot/issues/1901 ## 1.20.0 ### Added * Unicode data files updated to Unicode 16. Foot uses these to determine which VS-15 and VS-16 sequences are valid, and which are not. * Box drawing characters U+1CD00...U+1CDE5 (the _"octants"_ from the _"Symbols for Legacy Computing Supplement"_ codepoint range, added in Unicode 16.0). * `security.osc52` option, allowing you to partially or fully disable host clipboard access via the OSC-52 escape sequence ([#1867][1867]). [1867]: https://codeberg.org/dnkl/foot/issues/1867 ### Changed * OSC-9: sequences beginning with `;` are now ignored. These sequences are ConEmu/Windows Terminal sequences, and not intended to be notifications. * Use `utf8proc_charwidth()` instead of `wcwidth()`+`wcswidth()` when calculating character width, when foot has been built with utf8proc support ([#1865][1865]). * Run-time changes to the window title, and the app ID now require the new value to consist of printable characters only. * Kitty keyboard protocol: Enter, Tab and Backspace no longer report _release_ events unless _"Report all keys as escape codes"_ is enabled ([#1892][1892]). [1865]: https://codeberg.org/dnkl/foot/issues/1865 [1892]: https://codeberg.org/dnkl/foot/issues/1892 ### Fixed * Crash when receiving an OSC-9 or OSC-777 with an empty notification body ([#1866][1866]). * Crash when tripple-clicking on region containing `NUL` characters. [1866]: https://codeberg.org/dnkl/foot/issues/1866 ### Contributors * cy * Denis Zharikov * heather7283 * Jack Wilsdon * Mark Stosberg ## 1.19.0 ### Added * `resize-keep-grid` option, controlling whether the window is resized (and the grid reflowed) or not when e.g. zooming in/out ([#1807][1807]). * `strikeout-thickness` option. * Implemented the new `xdg-toplevel-icon-v1` protocol. * Implemented `CSI 21 t`: report window title. * `colors.sixelNN` option, controlling the default sixel color palette. [1807]: https://codeberg.org/dnkl/foot/issues/1807 ### Changed * `cursor.unfocused-style` is now effective even when `cursor.style` is not `block`. * Activating a notification triggered with OSC-777, or BEL, now focuses the foot window, if XDG activation tokens are supported by the compositor, the notification daemon, and the notification helper used by foot (i.e. `desktop-notifications.command`). This has been supported for OSC-99 since 1.18.0, and now we also support it for BEL and OSC-777 ([#1822][1822]). * Sixel background color (when `P2=0|2`) is now set to the **sixel** color palette entry #0, instead of using the current ANSI background color. This is what a real VT340 does. * The `.desktop` files no longer use the reverse DNS naming scheme, and their names now match the default app-ids used by foot (`foot` and `footclient`) ([#1607][1607]). * `file://` prefix are now stripped from OSC-8 URIs when activated/opened, **if** the hostname matches the hostname of the computer foot is running on ([#1840][1840]). [1822]: https://codeberg.org/dnkl/foot/issues/1822 [1607]: https://codeberg.org/dnkl/foot/issues/1607 [1840]: https://codeberg.org/dnkl/foot/issues/1840 ### Fixed * Some invalid UTF-8 strings passing the validity check when setting the window title, triggering a Wayland protocol error which then caused foot to shutdown. * "Too large" values for `scrollback.lines` causing an integer overflow, resulting in either visual glitches, crashes, or both ([#1828][1828]). * Crash when trying to set an invalid cursor shape with OSC-22, when foot uses server-side cursor shapes. * Occasional visual glitches when selecting text, when foot is running under a compositor that forces foot to double buffer (e.g. KDE/KWin) ([#1715][1715]). * Sixels flickering when foot is running under a compositor that forces foot to double buffer (e.g. KDE, or Smithay based compositors) ([#1851][1851]). [1828]: https://codeberg.org/dnkl/foot/issues/1828 [1715]: https://codeberg.org/dnkl/foot/issues/1715 [1851]: https://codeberg.org/dnkl/foot/issues/1851 ### Contributors * Andrew J. Hesford * Craig Barnes * Oleh Hushchenkov * tokyo4j ## 1.18.1 ### Added * OSC-99: support for the `s` parameter. Supported keywords are `silent`, `system` and names from the freedesktop sound naming specification. * `${muted}` and `${sound-name}` added to the `desktop-notifications.command` template. ### Changed * CSD buttons now activate on mouse button **release**, rather than press ([#1787][1787]). [1787]: https://codeberg.org/dnkl/foot/issues/1787 ### Fixed * Regression: OSC-111 not handling alpha changes correctly, causing visual glitches ([#1801][1801]). [1801]: https://codeberg.org/dnkl/foot/issues/1801 ### Contributors * Craig Barnes * Shogo Yamazaki ## 1.18.0 ### Added * `cursor.blink-rate` option, allowing you to configure the rate the cursor blinks with (when `cursor.blink=yes`) ([#1707][1707]); * Support for `wp_single_pixel_buffer_v1`; certain overlay surfaces will now utilize the new single-pixel buffer protocol. This mainly reduces the memory usage, but should also be slightly faster. * Support for high-res mouse wheel scroll events ([#1738][1738]). * Styled and colored underlines ([#828][828]). * Support for SGR 21 (double underline). * Support for `XTPUSHCOLORS`, `XTPOPCOLORS` and `XTREPORTCOLORS`, i.e. color palette stack ([#856][856]). * Log output now respects the [`NO_COLOR`](http://no-color.org/) environment variable ([#1771][1771]). * Support for [in-band window resize notifications](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83), private mode `2048`. * Support for OSC-99 [_"Kitty desktop notifications"_](https://sw.kovidgoyal.net/kitty/desktop-notifications/). * `desktop-notifications.command` option, replaces `notify`. * `desktop-notifications.inhibit-when-focused` option, replaces `notify-focus-inhibit`. * `${category}`, `${urgency}`, `${expire-time}`, `${replace-id}`, `${icon}` and `${action-argument}` added to the `desktop-notifications.command` template. * `desktop-notifications.command-action-argument` option, defining how `${action-argument}` (in `desktop-notifications.command`) should be expanded. * `desktop-notifications.close` option, defining what to execute when an application wants to close an existing notification (via an OSC-99 escape sequence). [1707]: https://codeberg.org/dnkl/foot/issues/1707 [1738]: https://codeberg.org/dnkl/foot/issues/1738 [828]: https://codeberg.org/dnkl/foot/issues/828 [856]: https://codeberg.org/dnkl/foot/issues/856 [1771]: https://codeberg.org/dnkl/foot/issues/1771 ### Changed * All `XTGETTCAP` capabilities are now in the `tigetstr()` format: - parameterized string capabilities were previously "source encoded", meaning e.g. `\E` where not "decoded" into `\x1b`. - Control characters were also "source encoded", meaning they were returned as e.g. "^G" instead of `\x07` ([#1701][1701]). In other words, if, after this change, `XTGETTCAP` returns a string that is different compared to `tigetstr()`, then it is likely a bug in foot's implementation of `XTGETTCAP`. * If the cursor foreground and background colors are identical (for example, when cursor uses inverted colors and the cell's foreground and background are the same), the cursor will instead be rendered using the default foreground and background colors, inverted ([#1761][1761]). * Mouse wheel events now generate `BTN_WHEEL_BACK` and `BTN_WHEEL_FORWARD` "button presses", instead of `BTN_BACK` and `BTN_FORWARD`. The default bindings have been updated, and `scrollback-up-mouse`, `scrollback-down-mouse`, `font-increase` and `font-decrease` now use the new button names. This change allow users to separate physical mouse buttons that _also_ generates `BTN_BACK` and `BTN_FORWARD`, from wheel scrolling ([#1763][1763]). * Replaced the old catppuccin theme with updated flavored themes pulled from [catppuccin/foot](https://github.com/catppuccin/foot) * Mouse selections can now be started inside the margins ([#1702][1702]). [1701]: https://codeberg.org/dnkl/foot/issues/1701 [1761]: https://codeberg.org/dnkl/foot/issues/1761 [1763]: https://codeberg.org/dnkl/foot/issues/1763 [1702]: https://codeberg.org/dnkl/foot/issues/1702 ### Deprecated * `notify` option; replaced by `desktop-notifications.command`. * `notify-focus-inhibit` option; replaced by `desktop-notifications.inhibit-when-focused`. ### Fixed * Crash when zooming in or out, with `dpi-aware=yes`, and the monitor's DPI is 0 (this is true for, for example, nested Wayland sessions, or in virtualized environments). * No error response for empty `XTGETTCAP` request ([#1694][1694]). * Unicode-mode in one foot client affecting other clients, in foot server mode ([#1717][1717]). * IME interfering in URL-mode ([#1718][1718]). * OSC-52 reply interleaved with other data sent to the client ([#1734][1734]). * XKB compose state being reset when foot receives a new keymap ([#1744][1744]). * Regression: alpha changes through OSC-11 sequences not taking effect until window is resized. * VS15 being ignored ([#1742][1742]). * VS16 being ignored for a subset of the valid VS16 sequences ([#1742][1742]). * Crash in debug builds, when using OSC-12 to set the cursor color and foot config has not set any custom cursor colors (i.e. without OSC-12, inverted fg/bg would be used). * Wrong color used when drawing the unfocused, hollow cursor. * Encoding of `BTN_BACK` and `BTN_FORWARD`, when sending a mouse input escape sequence to the terminal application. [1694]: https://codeberg.org/dnkl/foot/issues/1694 [1717]: https://codeberg.org/dnkl/foot/issues/1717 [1718]: https://codeberg.org/dnkl/foot/issues/1718 [1734]: https://codeberg.org/dnkl/foot/issues/1734 [1744]: https://codeberg.org/dnkl/foot/issues/1744 [1742]: https://codeberg.org/dnkl/foot/issues/1742 ### Contributors * abs3nt * Artturin * Craig Barnes * Jan Beich * Mariusz Bialonczyk * Nicolas Kolling Ribas ## 1.17.2 ### Changed * Notifications with invalid UTF-8 strings are now ignored. ### Fixed * Crash when changing aspect ratio of a sixel, in the middle of the sixel data (this is unsupported in foot, but should of course not result in a crash). * Crash when printing double-width (or longer) characters to, or near, the last column, when auto-wrap (private mode 7) has been disabled. * Dynamically sized sixel being trimmed to nothing. * Flickering with `dpi-aware=yes` and window is unmapped/remapped (some compositors do this when window is minimized), in a multi-monitor setup with different monitor DPIs. ## 1.17.1 ### Added * `cursor.unfocused-style=unchanged|hollow|none` to `foot.ini`. The default is `hollow` ([#1582][1582]). * New key binding: `quit` ([#1475][1475]). [1582]: https://codeberg.org/dnkl/foot/issues/1582 [1475]: https://codeberg.org/dnkl/foot/issues/1475 ### Fixed * Log-level not respected by syslog. * Regression: terminal shutting down when the PTY is closed by the client application, which may be earlier than when the client application exits ([#1666][1666]). * When closing the window, send `SIGHUP` to the client application, before sending `SIGTERM`. The signal sequence is now `SIGHUP`, wait, `SIGTERM`, wait `SIGKILL`. * Crash when receiving a `DECRQSS` request with more than 2 bytes in the `q` parameter. [1666]: https://codeberg.org/dnkl/foot/issues/1666 ### Contributors * Holger Weiß * izmyname * Marcin Puc * tunjan ## 1.17.0 ### Added - Support for opening an existing PTY, e.g. a VM console. ([#1564][1564]) * Unicode input mode now accepts input from the numpad as well, numlock is ignored. * A new `resize-by-cells` option, enabled by default, allows the size of floating windows to be constrained to multiples of the cell size. * Support for custom (i.e. other than ctrl/shift/alt/super) modifiers in key bindings ([#1348][1348]). * `pipe-command-output` key binding. * Support for OSC-176, _"Set App-ID"_ (https://gist.github.com/delthas/d451e2cc1573bb2364839849c7117239). * Support for `DECRQM` queries with ANSI/ECMA-48 modes (`CSI Ps $ p`). * Rectangular edit functions: `DECCARA`, `DECRARA`, `DECCRA`, `DECFRA` and `DECERA` ([#1633][1633]). * `Rect` capability to terminfo. * `fe` and `fd` (focus in/out enable/disable) capabilities to terminfo. * `nel` capability to terminfo. [1348]: https://codeberg.org/dnkl/foot/issues/1348 [1633]: https://codeberg.org/dnkl/foot/issues/1633 [1564]: https://codeberg.org/dnkl/foot/pulls/1564 [`DECBKM`]: https://vt100.net/docs/vt510-rm/DECBKM.html ### Changed * config: ARGB color values now default to opaque, rather than transparent, when the alpha component has been left out ([#1526][1526]). * The `foot` process now changes CWD to `/` after spawning the shell process. This ensures the terminal itself does not "lock" a directory; for example, preventing a mount point from being unmounted ([#1528][1528]). * Kitty keyboard protocol: updated behavior of modifiers bits during modifier key events, to match the (new [#6913][kitty-6913]) behavior in kitty >= 0.32.0 ([#1561][1561]). * When changing font sizes or display scales in floating windows, the window will be resized as needed to preserve the same grid size. * `smm` now disables private mode 1036 (_"send ESC when Meta modifies a key"_), and enables private mode 1034 (_"8-bit Meta mode"_). `rmm` does the opposite ([#1584][1584]). * Grid is now always centered in the window, when either fullscreened or maximized. * Ctrl+wheel up/down bound to `font-increase` and `font-decrease` respectively (in addition to the already existing default key bindings `ctrl-+` and `ctrl+-`). * Use XRGB pixel format (instead of ARGB) when there is no transparency. * Prefer CSS xcursor names, and fallback to legacy X11 names. * Kitty keyboard protocol: use the `XKB` mode when retrieving locked modifiers, instead of the `GTK` mode. This fixes an issue where some key combinations (e.g. Shift+space) produces different results depending on the state of e.g. the NumLock key. * Kitty keyboard protocol: filter out **all** locked modifiers (as reported by XKB), rather than hardcoding it to CapsLock only, when determining whether a key combination produces text or not. * CSI-t queries now report pixel values **unscaled**, instead of **scaled** ([#1643][1643]). * Sixel: text cursor is now placed on the last text row touched by the sixel, instead of the text row touched by the _upper_ pixel of the last sixel ([#chafa-192][chafa-192]). * Sixel: trailing, fully transparent rows are now trimmed ([#chafa-192][chafa-192]). * `1004` (enable focus in/out events) removed from the `XM` terminfo capability. To enable focus in/out, use the `fe` and `fd` capabilities instead. * Tightened the regular expression in the `rv` terminfo capability. * Tightened the regular expression in the `xr` terminfo capability. * `DECRQM` queries for private mode 67 ([`DECBKM`]) now reply with mode value 4 ("permanently reset") instead of 0 ("not recognized"). [1526]: https://codeberg.org/dnkl/foot/issues/1526 [1528]: https://codeberg.org/dnkl/foot/issues/1528 [1561]: https://codeberg.org/dnkl/foot/issues/1561 [kitty-6913]: https://github.com/kovidgoyal/kitty/issues/6913 [1584]: https://codeberg.org/dnkl/foot/issues/1584 [1643]: https://codeberg.org/dnkl/foot/issues/1643 [chafa-192]: https://github.com/hpjansson/chafa/issues/192 ### Fixed * config: improved validation of color values. * config: double close of file descriptor, resulting in a chain of errors ultimately leading to a startup failure ([#1531][1531]). * Crash when using a desktop scaling factor > 1, on compositors that implements neither the `fractional-scale-v1`, nor the `cursor-shape-v1` Wayland protocols ([#1573][1573]). * Crash in `--server` mode when one or more environment variables are set in `[environment]`. * Environment variables normally set by foot lost with `footclient -E,--client-environment` ([#1568][1568]). * XDG toplevel protocol violation, by trying to set a title that contains an invalid UTF-8 sequence ([#1552][1552]). * Crash when erasing the scrollback, when scrollback history is exactly 0 rows. This happens when `[scrollback].line = 0`, and the window size (number of rows) is a power of two (i.e. 2, 4, 8, 16 etc) ([#1610][1610]). * VS16 (variation selector 16 - emoji representation) should only affect emojis. * Pressing a modifier key while the kitty keyboard protocol is enabled no longer resets the viewport, or clears the selection. * Crash when failing to load an xcursor image ([#1624][1624]). * Crash when resizing a dynamically sized sixel (no raster attributes), with a non-1:1 aspect ratio. * The default sixel color table is now initialized to the colors used by the VT340, instead of not being initialized at all (thus requiring the sixel escape sequence to explicitly set all colors it used). [1531]: https://codeberg.org/dnkl/foot/issues/1531 [1573]: https://codeberg.org/dnkl/foot/issues/1573 [1568]: https://codeberg.org/dnkl/foot/issues/1568 [1552]: https://codeberg.org/dnkl/foot/issues/1552 [1610]: https://codeberg.org/dnkl/foot/issues/1610 [1624]: https://codeberg.org/dnkl/foot/issues/1624 ### Contributors * Alyssa Ross * Andrew J. Hesford * Artturin * Craig Barnes * delthas * eugenrh * Fazzi * Gregory Anders * Jan Palus * Leonardo Hernández Hernández * LmbMaxim * Matheus Afonso Martins Moreira * Sivecano * Tim Culverhouse * xnuk ## 1.16.2 ### Fixed * Last row and/or column of opaque sixels (not having a size that is a multiple of the cell size) being the wrong color ([#1520][1520]). [1520]: https://codeberg.org/dnkl/foot/issues/1520 ## 1.16.1 ### Fixed * Foot not starting on linux kernels before 6.3 ([#1514][1514]). * Cells underneath erased sixels not being repainted ([#1515][1515]). [1514]: https://codeberg.org/dnkl/foot/issues/1514 [1515]: https://codeberg.org/dnkl/foot/issues/1515 ## 1.16.0 ### Added * Support for building with _wayland-protocols_ as a subproject. * Mouse wheel scrolls can now be used in `mouse-bindings` ([#1077][1077]). * New mouse bindings: `scrollback-up-mouse` and `scrollback-down-mouse`, bound to `BTN_BACK` and `BTN_FORWARD` respectively. * New key binding: `select-quote`. This key binding selects text between quote characters, and falls back to selecting the entire row ([#1364][1364]). * Support for DECSET/DECRST/DECRQM 2027 (_Grapheme cluster processing_). * New **search mode** key bindings (along with their defaults) ([#419][419]): - `extend-char` (shift+right) - `extend-line-down` (shift+down) - `extend-backward-char` (shift+left) - `extend-backward-to-word-boundary` (ctrl+shift+left) - `extend-backward-to-next-whitespace` (none) - `extend-line-up` (shift+up) - `scrollback-up-page` (shift+page-up) - `scrollback-up-half-page` (none) - `scrollback-up-line` (none) - `scrollback-down-page` (shift+page-down) - `scrollback-down-half-page` (none) - `scrollback-down-line` (none) * Support for visual bell which flashes the terminal window. ([#1337][1337]). [1077]: https://codeberg.org/dnkl/foot/issues/1077 [1364]: https://codeberg.org/dnkl/foot/issues/1364 [419]: https://codeberg.org/dnkl/foot/issues/419 [1337]: https://codeberg.org/dnkl/foot/issues/1337 ### Changed * Minimum required version of _wayland-protocols_ is now 1.32 ([#1391][1391]). * `foot-server.service` systemd now checks for `ConditionEnvironment=WAYLAND_DISPLAY` for consistency with the socket unit ([#1448][1448]) * Default key binding for `select-row` is now `BTN_LEFT+4`. However, in many cases, triple clicking will still be enough to select the entire row; see the new key binding `select-quote` (mapped to `BTN_LEFT+3` by default) ([#1364][1364]). * `file://` prefix from URI's are no longer stripped when opened/activated ([#1474][1474]). * `XTGETTCAP` with capabilities that are not properly hex encoded will be ignored, instead of echo:ed back to the TTY in an error response. * Command line configuration overrides are now applied even if the configuration file does not exist or can't be parsed. ([#1495][1495]). * Wayland surface damage is now more fine-grained. This should result in lower latencies in many use cases, especially on high DPI monitors. [1391]: https://codeberg.org/dnkl/foot/issues/1391 [1448]: https://codeberg.org/dnkl/foot/pulls/1448 [1474]: https://codeberg.org/dnkl/foot/pulls/1474 [1495]: https://codeberg.org/dnkl/foot/pulls/1495 ### Removed * `utempter` config option (was deprecated in 1.15.0). ### Fixed * Race condition for systemd units start in GNOME and KDE ([#1436][1436]). * One frame being rendered at the wrong scale after being hidden by another opaque, maximized window ([#1464][1464]). * Double-width characters, and grapheme clusters breaking URL auto-detection ([#1465][1465]). * Crash when `XDG_ACTIVATION_TOKEN` is set, but compositor does not support XDG activation ([#1493][1493]). * Crash when compositor calls `fractional_scale::preferred_scale()` when there are no monitors (for example, after a monitor has been turned off and then back on again) ([#1498][1498]). * Transparency in margins (padding) not being disabled in fullscreen mode ([#1503][1503]). * Crash when a scrollback search match is in the last column. * Scrollback search: grapheme clusters not matching correctly. * Wrong baseline offset for some fonts ([#1511][1511]). [1436]: https://codeberg.org/dnkl/foot/issues/1436 [1464]: https://codeberg.org/dnkl/foot/issues/1464 [1465]: https://codeberg.org/dnkl/foot/issues/1465 [1493]: https://codeberg.org/dnkl/foot/pulls/1493 [1498]: https://codeberg.org/dnkl/foot/issues/1498 [1503]: https://codeberg.org/dnkl/foot/issues/1503 [1511]: https://codeberg.org/dnkl/foot/issues/1511 ### Contributors * 6t8k * Alyssa Ross * CismonX * Max Gautier * raggedmyth * Raimund Sacherer * Sertonix ## 1.15.3 ### Fixed * `-f,--font` command line option not affecting `csd.font` (if unset). * Vertical alignment in URL jump labels, and the scrollback position indicator. The fix in 1.15.2 was incorrect, and was reverted in the last minute. But we forgot to remove the entry from the changelog ([#1430][1430]). ## 1.15.2 ### Added * `[tweak].bold-text-in-bright-amount` option ([#1434][1434]). * `-Dterminfo-base-name` meson option, allowing you to name the terminfo files to something other than `-Ddefault-terminfo`. Use case: have foot default to using the terminfo from ncurses (`foot`, `foot-direct`), while still packaging foot's terminfo files, but under a different name (e.g. `foot-extra`, `foot-extra-direct`). [1434]: https://codeberg.org/dnkl/foot/issues/1434 ### Fixed * Crash when copying text that contains invalid UTF-8 ([#1423][1423]). * Wrong font size after suspending the monitor ([#1431][1431]). * Vertical alignment in URL jump labels, and the scrollback position indicator ([#1430][1430]). * Regression: line- and box drawing characters not covering the full height of the line, when a custom `line-height` is being used ([#1430][1430]). * Crash when compositor does not implement the _viewporter_ interface ([#1444][1444]). * CSD rendering with fractional scaling ([#1441][1441]). * Regression: crash with certain combinations of `--window-size-chars=NxM` and desktop scaling factors ([#1446][1446]). [1423]: https://codeberg.org/dnkl/foot/issues/1423 [1431]: https://codeberg.org/dnkl/foot/issues/1431 [1430]: https://codeberg.org/dnkl/foot/issues/1430 [1444]: https://codeberg.org/dnkl/foot/issues/1444 [1441]: https://codeberg.org/dnkl/foot/issues/1441 [1446]: https://codeberg.org/dnkl/foot/issues/1446 ## 1.15.1 ### Changed * When window is mapped, use metadata (DPI, scaling factor, subpixel configuration) from the monitor we were most recently mapped on, instead of the one least recently. * Starlight theme (the default theme) updated to [V4][starlight-v4] * Background transparency (alpha) is now disabled in fullscreened windows ([#1416][1416]). * Foot server systemd units now use the standard graphical-session.target ([#1281][1281]). * If `$XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock` does not exist, `footclient` now tries `$XDG_RUNTIME_DIR/foot.sock`, then `/tmp/foot.sock`, even if `$WAYLAND_DISPLAY` and/or `$XDG_RUNTIME_DIR` are defined ([#1281][1281]). * Font baseline calculation: try to center the text within the line, instead of anchoring it at the top ([#1302][1302]). [starlight-v4]: https://github.com/CosmicToast/starlight/blob/v4/CHANGELOG.md#v4 [1416]: https://codeberg.org/dnkl/foot/issues/1416 [1281]: https://codeberg.org/dnkl/foot/pulls/1281 [1302]: https://codeberg.org/dnkl/foot/issues/1302 ### Fixed * Use appropriate rounding when applying fractional scales. * Xcursor not being scaled correctly on `fractional-scale-v1` capable compositors. * `dpi-aware=yes` being broken on `fractional-scale-v1` capable compositors (and when a fractional scaling factor is being used) ([#1404][1404]). * Initial font size being wrong on `fractional-scale-v1` capable compositors, with multiple monitors with different scaling factors connected ([#1404][1404]). * Crash when _pointer capability_ is removed from a seat, on compositors without `cursor-shape-v1 support` ([#1411][1411]). * Crash on exit, if the mouse is hovering over the foot window (does not happen on all compositors) * Visual glitches when CSD titlebar is transparent. [1404]: https://codeberg.org/dnkl/foot/issues/1404 [1411]: https://codeberg.org/dnkl/foot/pulls/1411 ### Contributors * Ayush Agarwal * CismonX * Max Gautier * Ronan Pigott * xdavidwu ## 1.15.0 ### Added * VT: implemented `XTQMODKEYS` query (`CSI ? Pp m`). * Meson option `utmp-backend=none|libutempter|ulog|auto`. The default is `auto`, which will select `libutempter` on Linux, `ulog` on FreeBSD, and `none` for all others. * Sixel aspect ratio. * Support for the new `fractional-scale-v1` Wayland protocol. This brings true fractional scaling to Wayland in general, and with this release, to foot. * Support for the new `cursor-shape-v1` Wayland protocol, i.e. server side cursor shapes ([#1379][1379]). * Support for touchscreen input ([#517][517]). * `csd.double-click-to-maximize` option to `foot.ini`. Defaults to `yes` ([#1293][1293]). [1379]: https://codeberg.org/dnkl/foot/issues/1379 [517]: https://codeberg.org/dnkl/foot/issues/517 [1293]: https://codeberg.org/dnkl/foot/issues/1293 ### Changed * Default color theme is now [starlight](https://github.com/CosmicToast/starlight) ([#1321][1321]). * Minimum required meson version is now 0.59 ([#1371][1371]). * `Control+Shift+u` is now bound to `unicode-input` instead of `show-urls-launch`, to follow the convention established in GTK and Qt ([#1183][1183]). * `show-urls-launch` now bound to `Control+Shift+o` ([#1183][1183]). * Kitty keyboard protocol: F3 is now encoded as `CSI 13~` instead of `CSI R`. The kitty keyboard protocol originally allowed F3 to be encoded as `CSI R`, but this was removed from the specification since `CSI R` conflicts with the _"Cursor Position Report"_. * `[main].utempter` renamed to `[main].utmp-helper`. The old option name is still recognized, but will log a deprecation warning. * Meson option `default-utempter-path` renamed to `utmp-default-helper-path`. * Opaque sixels now retain the background opacity (when current background color is the **default** background color) ([#1360][1360]). * Text cursor's vertical position after emitting a sixel, when sixel scrolling is **enabled** (the default) has been updated to match XTerm's, and the VT382's behavior: the cursor is positioned **on** the last sixel row, rather than _after_ it. This allows printing sixels on the last row without scrolling up, but also means applications may have to explicitly emit a newline to ensure the sixel is visible. For example, `cat`:ing a sixel in the shell will typically result in the last row not being visible, unless a newline is explicitly added. * Default sixel aspect ratio is now 2:1 instead of 1:1. * Sixel images are no longer cropped to the last non-transparent row. * Sixel images are now re-scaled when the font size is changed ([#1383][1383]). * `dpi-aware` now defaults to `no`, and the `auto` value has been removed. * When using custom cursor colors (`cursor.color` is set in `foot.ini`), the cursor is no longer inverted when the cell is selected, or when the cell has the `reverse` (SGR 7) attribute set ([#1347][1347]). [1321]: https://codeberg.org/dnkl/foot/issues/1321 [1371]: https://codeberg.org/dnkl/foot/pulls/1371 [1183]: https://codeberg.org/dnkl/foot/issues/1183 [1360]: https://codeberg.org/dnkl/foot/issues/1360 [1383]: https://codeberg.org/dnkl/foot/issues/1383 [1347]: https://codeberg.org/dnkl/foot/issues/1347 ### Deprecated * `[main].utempter` option. ### Removed * `auto` value for the `dpi-aware` option. ### Fixed * Incorrect icon in dock and window switcher on Gnome ([#1317][1317]) * Crash when scrolling after resizing the window with non-zero scrolling regions. * `XTMODKEYS` state not being reset on a terminal reset. * In Gnome dock foot always groups under "foot client". Change instances of footclient and foot to appear as "foot client" and "foot" respectively. ([#1355][1355]). * Glitchy rendering when alpha (transparency) is changed between opaque and non-opaque at runtime (using OSC-11). * Regression: crash when resizing the window when `resize-delay-ms > 0` ([#1377][1377]). * Crash when scrolling up while running something that generates a lot of output (for example, `yes`) ([#1380][1380]). * Default key binding for URL mode conflicting with Unicode input on some DEs; `show-urls-launched` is now mapped to `Control+Shift+o` by default, instead of `Control+Shift+u` ([#1183][1183]). [1317]: https://codeberg.org/dnkl/foot/issues/1317 [1355]: https://codeberg.org/dnkl/foot/issues/1355 [1377]: https://codeberg.org/dnkl/foot/issues/1377 [1380]: https://codeberg.org/dnkl/foot/issues/1380 ### Contributors * Antoine Beaupré * CismonX * Craig Barnes * Dan Bungert * jdevdevdev * Kyle Gunger * locture * Phillip Susi * sewn * ShugarSkull * Vivian Szczepanski * Vladimir Bauer * wout * CosmicToast ## 1.14.0 ### Added * Support for adjusting the thickness of regular underlines ([#1136][1136]). * Support (optional) for utmp logging with libutempter. * `kxIN` and `kxOUT` (focus in/out events) to terminfo. * `name` capability to `XTGETTCAP`. * String values in `foot.ini` may now be quoted. This can be used to set a value to the empty string, for example. * Environment variables can now be **unset**, by setting `[environment].=""` (quotes are required) ([#1225][1225]). * `font-size-adjustment=N[px]` option, letting you configure how much to increment/decrement the font size when zooming in or out ([#1188][1188]). * Bracketed paste terminfo entries (`BD`, `BE`, `PE` and `PS`, added to ncurses in 2022-12-24). Vim makes use of these. * "Report version" terminfo entries (`XR`/`xr`). * "Report DA2" terminfo entries (`RV`/`rv`). * `XF` terminfo capability (focus in/out events available). * `$TERM_PROGRAM` and `$TERM_PROGRAM_VERSION` environment variables unset in the slave process. [1136]: https://codeberg.org/dnkl/foot/issues/1136 [1225]: https://codeberg.org/dnkl/foot/issues/1225 [1188]: https://codeberg.org/dnkl/foot/issues/1188 ### Changed * Default color theme from a variant of the Zenburn theme, to a variant of the Solarized dark theme. * Default `pad` from 2x2 to 0x0 (i.e. no padding at all). * Current working directory (as set by OSC-7) is now passed to the program executed by the `pipe-*` key bindings ([#1166][1166]). * `DECRPM` replies (to `DECRQM` queries) now report a value of `4` ("permanently reset") instead of `2` ("reset") for DEC private modes that are known but unsupported. * Set `PWD` environment variable in the slave process ([#1179][1179]). * DPI is now forced to 96 when found to be unreasonably high. * Set default log level to warning ([#1215][1215]). * Default `grapheme-width-method` from `wcswidth` to `double-width`. * When determining initial font size, do FontConfig config substitution if the user-provided font pattern has no {pixel}size option ([#1287][1287]). * DECRST of DECCOLM and DECSCLM removed from terminfo. [1166]: https://codeberg.org/dnkl/foot/issues/1166 [1179]: https://codeberg.org/dnkl/foot/issues/1179 [1215]: https://codeberg.org/dnkl/foot/pulls/1215 [1287]: https://codeberg.org/dnkl/foot/issues/1287 ### Fixed * Crash in `foot --server` on key press, after another `footclient` has terminated very early (for example, by trying to launch a non-existing shell/client). * Glitchy rendering when scrolling in the scrollback, on compositors that does not allow Wayland buffer reuse (e.g. KDE/plasma) ([#1173][1173]) * Scrollback search matches not being highlighted correctly, on compositors that does not allow Wayland buffer reuse (e.g. KDE/plasma). * Nanosecs "overflow" when calculating timeout value for `resize-delay-ms` option. * Missing backslash in ST terminator in escape sequences in the built-in terminfo (accessed via XTGETTCAP). * Crash when interactively resizing the window with a very large scrollback. * Crash when a sixel image exceeds the current sixel max height. * Crash after reverse-scrolling (`CSI Ps T`) in the 'normal' (non-alternate) screen ([#1190][1190]). * Background transparency being applied to the text "behind" the cursor. Only applies to block cursor using inversed fg/bg colors. ([#1205][1205]). * Crash when monitor's physical size is "too small" ([#1209][1209]). * Line-height adjustment when incrementing/decrementing the font size with a user-set line-height ([#1218][1218]). * Scaling factor not being correctly applied when converting pt-or-px config values (e.g. letter offsets, line height etc). * Selection being stuck visually when `IL` and `DL`. * URL underlines sometimes still being visible after exiting URL mode. * Text-bindings, and pipe-* bindings, with multiple key mappings causing a crash (double-free) on exit ([#1259][1259]). * Double-width glyphs glitching when surrounded by glyphs overflowing into the double-width glyph ([#1256][1256]). * Wayland protocol violation when ack:ing a configure event for an unmapped surface ([#1249][1249]). * `xdg_toplevel::set_min_size()` not being called. * Key bindings with consumed modifiers masking other key bindings ([#1280][1280]). * Multi-character compose sequences with the kitty keyboard protocol ([#1288][1288]). * Crash when application output scrolls very fast, e.g. `yes` ([#1305][1305]). * Crash when application scrolls **many** lines (> ~2³¹). * DECCOLM erasing the screen ([#1265][1265]). [1173]: https://codeberg.org/dnkl/foot/issues/1173 [1190]: https://codeberg.org/dnkl/foot/issues/1190 [1205]: https://codeberg.org/dnkl/foot/issues/1205 [1209]: https://codeberg.org/dnkl/foot/issues/1209 [1218]: https://codeberg.org/dnkl/foot/issues/1218 [1259]: https://codeberg.org/dnkl/foot/issues/1259 [1256]: https://codeberg.org/dnkl/foot/issues/1256 [1249]: https://codeberg.org/dnkl/foot/issues/1249 [1280]: https://codeberg.org/dnkl/foot/issues/1280 [1288]: https://codeberg.org/dnkl/foot/issues/1288 [1305]: https://codeberg.org/dnkl/foot/issues/1305 [1265]: https://codeberg.org/dnkl/foot/issues/1265 ### Contributors * Alexey Sakovets * Andrea Pappacoda * Antoine Beaupré * argosatcore * Craig Barnes * EuCaue * Grigory Kirillov * Harri Nieminen * Hugo Osvaldo Barrera * jaroeichler * Joakim Nohlgård * Nick Hastings * Soren A D * Torsten Trautwein * Vladimír Magyar * woojiq * Yorick Peterse ## 1.13.1 ### Changed * Window is now dimmed while in Unicode input mode. ### Fixed * Compiling against wayland-protocols < 1.25 * Crash on buggy compositors (GNOME) that sometimes send pointer-enter events with a NULL surface. Foot now ignores these events, and the subsequent motion and leave events. * Regression: "random" selected empty cells being highlighted as selected when they should not. * Crash when either resizing the terminal window, or scrolling in the scrollback history ([#1074][1074]) * OSC-8 URLs with matching IDs, but mismatching URIs being incorrectly connected. [1074]: https://codeberg.org/dnkl/foot/pulls/1074 ## 1.13.0 ### Added * XDG activation support when opening URLs ([#1058][1058]). * `-Dsystemd-units-dir=` meson command line option. * Support for custom environment variables in `foot.ini` ([#1070][1070]). * Support for jumping to previous/next prompt (requires shell integration). By default bound to `ctrl`+`shift`+`z` and `ctrl`+`shift`+`x` respectively ([#30][30]). * `colors.search-box-no-match` and `colors.search-box-match` options to `foot.ini` ([#1112][1112]). * Very basic Unicode input mode via the new `key-bindings.unicode-input` and `search-bindings.unicode-input` key bindings. Note that there is no visual feedback, as the preferred way of entering Unicode characters is with an IME ([#1116][1116]). * Support for `xdg_toplevel.wm_capabilities`, to adapt the client-side decoration buttons to the compositor capabilities ([#1061][1061]). [1058]: https://codeberg.org/dnkl/foot/issues/1058 [1070]: https://codeberg.org/dnkl/foot/issues/1070 [30]: https://codeberg.org/dnkl/foot/issues/30 [1112]: https://codeberg.org/dnkl/foot/issues/1112 [1116]: https://codeberg.org/dnkl/foot/issues/1116 [1061]: https://codeberg.org/dnkl/foot/pulls/1061 ### Changed * Use `$HOME` instead of `getpwuid()` to retrieve the user's home directory when searching for `foot.ini`. * HT, VT and FF are no longer stripped when pasting in non-bracketed mode ([#1084][1084]). * NUL is now stripped when pasting in non-bracketed mode ([#1084][1084]). * `alt`+`escape` now emits `\E\E` instead of a `CSI 27` sequence ([#1105][1105]). [1084]: https://codeberg.org/dnkl/foot/issues/1084 [1105]: https://codeberg.org/dnkl/foot/issues/1105 ### Fixed * Graphical corruption when viewport is at the top of the scrollback, and the output is scrolling. * Improved text reflow of logical lines with trailing empty cells ([#1055][1055]) * IME focus is now tracked independently from keyboard focus. * Workaround for buggy compositors (e.g. some versions of GNOME) allowing drag-and-drops even though foot has reported it does not support the offered mime-types ([#1092][1092]). * Keyboard enter/leave events being ignored if there is no keymap ([#1097][1097]). * Crash when application emitted an invalid `CSI 38;5;m`, `CSI 38:5:m`, `CSI 48;5;m` or `CSI 48:5:m` sequence ([#1111][1111]). * Certain dead-key combinations resulting in different escape sequences compared to kitty, when the kitty keyboard protocol is used ([#1120][1120]). * Search matches ending with a double-width character not being highlighted correctly. * Selection not being cancelled correctly when scrolled out. * Extending a multi-page selection behaving inconsistently. * Poor performance when making very large selections ([#1114][1114]). * Bogus error message when using systemd socket activation for server mode ([#1107][1107]) * Empty line at the bottom after a window resize ([#1108][1108]). [1055]: https://codeberg.org/dnkl/foot/issues/1055 [1092]: https://codeberg.org/dnkl/foot/issues/1092 [1097]: https://codeberg.org/dnkl/foot/issues/1097 [1111]: https://codeberg.org/dnkl/foot/issues/1111 [1120]: https://codeberg.org/dnkl/foot/issues/1120 [1114]: https://codeberg.org/dnkl/foot/issues/1114 [1107]: https://codeberg.org/dnkl/foot/issues/1107 [1108]: https://codeberg.org/dnkl/foot/issues/1108 ### Contributors * Craig Barnes * Lorenz * Max Gautier * Simon Ser * Stefan Prosiegel ## 1.12.1 ### Added * Workaround for Sway bug [#6960][sway-6960]: scrollback search and the OSC-555 ("flash") escape sequence leaves dimmed (search) and yellow (flash) artifacts ([#1046][1046]). * `Control+Shift+v` and `XF86Paste` have been added to the default set of key bindings that paste from the clipboard into the scrollback search buffer. This is in addition to the pre-existing `Control+v` and `Control+y` bindings. [sway-6960]: https://github.com/swaywm/sway/issues/6960 [1046]: https://codeberg.org/dnkl/foot/issues/1046 ### Changed * Scrollback search's `extend-to-word-boundary` no longer stops at space-to-word boundaries, making selection extension feel more natural. ### Fixed * build: missing symbols when linking the `pgo` helper binary. * UI not refreshing when pasting something into the scrollback search box, that does not result in a grid update (for example, when the search criteria did not result in any matches) ([#1040][1040]). * foot freezing in scrollback search mode, using 100% CPU ([#1036][1036], [#1047][1047]). * Crash when extending a selection to the next word boundary in scrollback search mode ([#1036][1036]). * Scrollback search mode not always highlighting all matches correctly. * Sixel options not being reset on hard resets (`\Ec`) [1040]: https://codeberg.org/dnkl/foot/issues/1040 [1036]: https://codeberg.org/dnkl/foot/issues/1036 [1047]: https://codeberg.org/dnkl/foot/issues/1047 ## 1.12.0 ### Added * OSC-22 - set xcursor pointer. * Add "xterm" as fallback cursor where "text" is not available. * `[key-bindings].scrollback-home|end` options. * Socket activation for `foot --server` and accompanying systemd unit files * Support for re-mapping input, i.e. mapping input to custom escape sequences ([#325][325]). * Support for [DECNKM](https://vt100.net/docs/vt510-rm/DECNKM.html), which allows setting/saving/restoring/querying the keypad mode. * Sixel support can be disabled by setting `[tweak].sixel=no` ([#950][950]). * footclient: `-E,--client-environment` command line option. When used, the child process in the new terminal instance inherits the environment from the footclient process instead of the server's ([#1004][1004]). * `[csd].hide-when-maximized=yes|no` option ([#1019][1019]). * Scrollback search mode now highlights all matches. * `[key-binding].show-urls-persistent` action. This key binding action is similar to `show-urls-launch`, but does not automatically exit URL mode after activating an URL ([#964][964]). * Support for `CSI > 4 n`, disable _modifyOtherKeys_. Note that since foot only supports level 1 and 2 (and not level 0), this sequence does not disable _modifyOtherKeys_ completely, but simply reverts it back to level 1 (the default). * `-Dtests=false|true` meson command line option. When disabled, test binaries will neither be built, nor will `ninja test` attempt to execute them. Enabled by default ([#919][919]). [325]: https://codeberg.org/dnkl/foot/issues/325 [950]: https://codeberg.org/dnkl/foot/issues/950 [1004]: https://codeberg.org/dnkl/foot/issues/1004 [1019]: https://codeberg.org/dnkl/foot/issues/1019 [964]: https://codeberg.org/dnkl/foot/issues/964 [919]: https://codeberg.org/dnkl/foot/issues/919 ### Changed * Minimum required meson version is now 0.58. * Mouse selections are now finalized when the window is resized ([#922][922]). * OSC-4 and OSC-11 replies now uses four digits instead of 2 ([#971][971]). * `\r` is no longer translated to `\n` when pasting clipboard data ([#980][980]). * Use circles for rendering light arc box-drawing characters ([#988][988]). * Example configuration is now installed to `${sysconfdir}/xdg/foot/foot.ini`, typically resolving to `/etc/xdg/foot/foot.ini` ([#1001][1001]). [922]: https://codeberg.org/dnkl/foot/issues/922 [971]: https://codeberg.org/dnkl/foot/issues/971 [980]: https://codeberg.org/dnkl/foot/issues/980 [988]: https://codeberg.org/dnkl/foot/issues/988 [1001]: https://codeberg.org/dnkl/foot/issues/1001 ### Removed * DECSET mode 27127 (which was first added in release 1.6.0). The kitty keyboard protocol (added in release 1.10.3) can be used to similar effect. ### Fixed * Build: missing `wayland_client` dependency in `test-config` ([#918][918]). * "(null)" being logged as font-name (for some fonts) when warning about a non-monospaced primary font. * Rare crash when the window is resized while a mouse selection is ongoing ([#922][922]). * Large selections crossing the scrollback wrap-around ([#924][924]). * Crash in `pipe-scrollback` ([#926][926]). * Exit code being 0 when a foot server with no open windows terminate due to e.g. a Wayland connection failure ([#943][943]). * Key binding collisions not detected for bindings specified as option overrides on the command line. * Crash when seat has no keyboard ([#963][963]). * Key presses with e.g. `AltGr` triggering key combinations with the base symbol ([#983][983]). * Underline cursor sometimes being positioned too low, either making it look thinner than what it should be, or being completely invisible ([#1005][1005]). * Fallback to `/etc/xdg` if `XDG_CONFIG_DIRS` is unset ([#1008][1008]). * Improved compatibility with XTerm when `modifyOtherKeys=2` ([#1009][1009]). * Window geometry when CSDs are enabled and CSD border width set to a non-zero value. This fixes window snapping in e.g. GNOME. * Window size "jumping" when starting an interactive resize when CSDs are enabled, and CSD border width set to a non-zero value. * Key binding overrides on the command line having no effect with `footclient` instances ([#931][931]). * Search prev/next not updating the selection correctly when the previous and new match overlaps. * Various minor fixes to scrollback search, and how it finds the next/prev match. [918]: https://codeberg.org/dnkl/foot/issues/918 [922]: https://codeberg.org/dnkl/foot/issues/922 [924]: https://codeberg.org/dnkl/foot/issues/924 [926]: https://codeberg.org/dnkl/foot/issues/926 [943]: https://codeberg.org/dnkl/foot/issues/943 [963]: https://codeberg.org/dnkl/foot/issues/963 [983]: https://codeberg.org/dnkl/foot/issues/983 [1005]: https://codeberg.org/dnkl/foot/issues/1005 [1008]: https://codeberg.org/dnkl/foot/issues/1008 [1009]: https://codeberg.org/dnkl/foot/issues/1009 [931]: https://codeberg.org/dnkl/foot/issues/931 ### Contributors * Ashish SHUKLA * Craig Barnes * Enes Hecan * Johannes Altmanninger * L3MON4D3 * Leonardo Neumann * Mariusz Bialonczyk * Max Gautier * Merlin Büge * jvoisin * merkix ## 1.11.0 ### Added * `[mouse-bindings].selection-override-modifiers` option, specifying which modifiers to hold to override mouse grabs by client applications and force selection instead. * _irc://_ and _ircs://_ to the default set of protocols recognized when auto-detecting URLs. * [SGR-Pixels (1016) mouse extended coordinates](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates) is now supported ([#762](https://codeberg.org/dnkl/foot/issues/762)). * `XTGETTCAP` - builtin terminfo. See [README.md::XTGETTCAP](README.md#xtgettcap) for details ([#846](https://codeberg.org/dnkl/foot/issues/846)). * `DECRQSS` - _Request Selection or Setting_ ([#798](https://codeberg.org/dnkl/foot/issues/798)). Implemented settings are: - `DECSTBM` - _Set Top and Bottom Margins_ - `SGR` - _Set Graphic Rendition_ - `DECSCUSR` - _Set Cursor Style_ * Support for searching for the last searched-for string in scrollback search (search for next/prev match with an empty search string). ### Changed * PaperColorDark and PaperColorLight themes renamed to paper-color-dark and paper-color-light, for consistency with other theme names. * `[scrollback].multiplier` is now applied in "alternate scroll" mode, where scroll events are translated to fake arrow key presses on the alt screen ([#859](https://codeberg.org/dnkl/foot/issues/859)). * The width of the block cursor's outline in an unfocused window is now scaled by the output scaling factor ("desktop scaling"). Previously, it was always 1px. * Foot will now try to change the locale to either "C.UTF-8" or "en_US.UTF-8" if started with a non-UTF8 locale. If this fails, foot will start, but only to display a window with an error (user's shell is not executed). * `gettimeofday()` has been replaced with `clock_gettime()`, due to it being marked as obsolete by POSIX. * `alt+tab` now emits `ESC \t` instead of `CSI 27;3;9~` ([#900](https://codeberg.org/dnkl/foot/issues/900)). * File pasted, or dropped, on the alt screen is no longer quoted ([#379](https://codeberg.org/dnkl/foot/issues/379)). * Line-based selections now include a trailing newline when copied ([#869](https://codeberg.org/dnkl/foot/issues/869)). * Foot now clears the signal mask and resets all signal handlers to their default handlers at startup ([#854](https://codeberg.org/dnkl/foot/issues/854)). * `Copy` and `Paste` keycodes are supported by default for the clipboard. These are useful for keyboards with custom firmware like QMK to enable global copy/paste shortcuts that work inside and outside the terminal (https://codeberg.org/dnkl/foot/pulls/894). ### Removed * Workaround for slow resize in Sway <= 1.5, when a foot window was hidden, for example, in a tabbed view (https://codeberg.org/dnkl/foot/pulls/507). ### Fixed * Font size adjustment ("zooming") when font is configured with a **pixelsize**, and `dpi-aware=no` ([#842](https://codeberg.org/dnkl/foot/issues/842)). * Key presses triggering keyboard layout switches also emitting CSI codes in the Kitty keyboard protocol. * Assertion in `shm.c:buffer_release()` ([#844](https://codeberg.org/dnkl/foot/issues/844)). * Crash when setting a key- or mouse binding to the empty string ([#851](https://codeberg.org/dnkl/foot/issues/851)). * Crash when maximizing the window and `[csd].size=1` ([#857](https://codeberg.org/dnkl/foot/issues/857)). * OSC-8 URIs not getting overwritten (erased) by double-width characters (e.g. emojis). * Rendering of CSD borders when `csd.border-width > 0` and desktop scaling has been enabled. * Failure to launch when `exec(3)`:ed with an empty argv. * Pasting from the primary clipboard (mouse middle clicking) did not reset the scrollback view to the bottom. * Wrong mouse binding triggered when doing two mouse selections in very quick (< 300ms) succession ([#883](https://codeberg.org/dnkl/foot/issues/883)). * Bash completion giving an error when completing a list of short options * Sixel: large image resizes (triggered by e.g. large repeat counts in `DECGRI`) are now truncated instead of ignored. * Sixel: a repeat count of 0 in `DECGRI` now emits a single sixel. * LIGHT ARC box drawing characters incorrectly rendered platforms ([#914](https://codeberg.org/dnkl/foot/issues/914)). ### Contributors * [lamonte](https://codeberg.org/lamonte) * Érico Nogueira * feeptr * Felix Lechner * grtcdr * Mark Stosberg * Nicolai Dagestad * Oğuz Ersen * Pranjal Kole * Simon Ser ## 1.10.3 ### Added * Kitty keyboard protocol ([#319](https://codeberg.org/dnkl/foot/issues/319)): - [Report event types](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-events) (mode `0b10`) - [Report alternate keys](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-alternates) (mode `0b100`) - [Report all keys as escape codes](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-all-keys) (mode `0b1000`) - [Report associated text](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-text) (mode `0b10000`) ### Fixed * Crash when bitmap fonts are scaled down to very small font sizes ([#830](https://codeberg.org/dnkl/foot/issues/830)). * Crash when overwriting/erasing an OSC-8 URL. ## 1.10.2 ### Added * New value, `max`, for `[tweak].grapheme-width-method`. * Initial support for the [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/). Modes supported: - [Disambiguate escape codes](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#disambiguate) (mode `0b1`) * "Window menu" (compositor provided) on right clicks on the CSD title bar. ### Fixed * An ongoing mouse selection is now finalized on a pointer leave event (for example by switching workspace while doing a mouse selection). * OSC-8 URIs in the last column * OSC-8 URIs sometimes being applied to too many, and seemingly unrelated cells ([#816](https://codeberg.org/dnkl/foot/issues/816)). * OSC-8 URIs incorrectly being dropped when resizing the terminal window with the alternate screen active. * CSD border not being dimmed when window is not focused. * Visual corruption with large CSD borders ([#823](https://codeberg.org/dnkl/foot/issues/823)). * Mouse cursor shape sometimes not being updated correctly. * Color palette changes (via OSC 4/104) no longer affect RGB colors ([#678](https://codeberg.org/dnkl/foot/issues/678)). ### Contributors * Jonas Ådahl ## 1.10.1 ### Added * `-Dthemes=false|true` meson command line option. When disabled, example theme files are **not** installed. * XDG desktop file for footclient. ### Fixed * Regression: `letter-spacing` resulting in a "not a valid option" error ([#795](https://codeberg.org/dnkl/foot/issues/795)). * Regression: bad section name in configuration error messages. * Regression: `pipe-*` key bindings not being parsed correctly, resulting in invalid error messages ([#809](https://codeberg.org/dnkl/foot/issues/809)). * OSC-8 data not being cleared when cell is overwritten ([#804](https://codeberg.org/dnkl/foot/issues/804), [#801](https://codeberg.org/dnkl/foot/issues/801)). ### Contributors * Arnavion * Craig Barnes * Soc Virnyl Silab Estela * Xiretza ## 1.10.0 ### Added * `notify-focus-inhibit` boolean option, which can be used to control whether desktop notifications should be inhibited when the terminal has keyboard focus * `[colors].scrollback-indicator` color-pair option, which specifies foreground and background colors for the scrollback indicator. * `[key-bindings].noop` action. Key combinations assigned to this action will not be sent to the application ([#765](https://codeberg.org/dnkl/foot/issues/765)). * Color schemes are now installed to `${datadir}/foot/themes`. * `[csd].border-width` and `[csd].border-color`, allowing you to configure the width and color of the CSD border. * Support for `XTMODKEYS` with `Pp=4` and `Pv=2` (_modifyOtherKeys=2_). * `[colors].dim0-7` options, allowing you to configure custom "dim" colors ([#776](https://codeberg.org/dnkl/foot/issues/776)). ### Changed * `[tweak].grapheme-shaping` is now enabled by default when both foot itself, and fcft has been compiled with support for it. * Default value of `[tweak].grapheme-width-method` changed from `double-width` to `wcswidth`. * INSTALL.md: `--override tweak.grapheme-shaping=no` added to PGO command line. * Foot now terminates if there are no available seats - for example, due to the compositor not implementing a recent enough version of the `wl_seat` interface ([#779](https://codeberg.org/dnkl/foot/issues/779)). * Boolean options in `foot.ini` are now limited to "yes|true|on|1|no|false|off|0", Previously, anything that did not match "yes|true|on", or a number greater than 0, was treated as "false". * `[scrollback].multiplier` is no longer applied when the alternate screen is in use ([#787](https://codeberg.org/dnkl/foot/issues/787)). ### Removed * The bundled PKGBUILD. * Deprecated `bell` option (replaced with `[bell]` section in 1.8.0). * Deprecated `url-launch`, `jump-label-letters` and `osc8-underline` options (moved to a dedicated `[url]` section in 1.8.0) ### Fixed * 'Sticky' modifiers in input handling; when determining modifier state, foot was looking at **depressed** modifiers, not **effective** modifiers, like it should. * Fix crashes after enabling CSD at runtime when `csd.size` is 0. * Convert `\r` to `\n` when reading clipboard data ([#752](https://codeberg.org/dnkl/foot/issues/752)). * Clipboard occasionally ceasing to work, until window has been re-focused ([#753](https://codeberg.org/dnkl/foot/issues/753)). * Don't propagate window title updates to the Wayland compositor unless the new title is different from the old title. ### Contributors * armin * Craig Barnes * Daniel Martí * feeptr * Mitja Horvat * Ronan Pigott * Stanislav Ochotnický ## 1.9.2 ### Changed * PGO helper scripts no longer set `LC_CTYPE=en_US.UTF-8`. But, note that "full" PGO builds still **require** a UTF-8 locale; you need to set one manually in your build script ([#728](https://codeberg.org/dnkl/foot/issues/728)). ## 1.9.1 ### Added * Warn when it appears the primary font is not monospaced. Can be disabled by setting `[tweak].font-monospace-warn=no` ([#704](https://codeberg.org/dnkl/foot/issues/704)). * PGO build scripts, in the `pgo` directory. See INSTALL.md - _Performance optimized, PGO_, for details ([#701](https://codeberg.org/dnkl/foot/issues/701)). * Braille characters (U+2800 - U+28FF) are now rendered by foot itself ([#702](https://codeberg.org/dnkl/foot/issues/702)). * `-e` command-line option. This option is simply ignored, to appease program launchers that blindly pass `-e` to any terminal emulator ([#184](https://codeberg.org/dnkl/foot/issues/184)). ### Changed * `-Ddefault-terminfo` is now also applied to the generated terminfo definitions when `-Dterminfo=enabled`. * `-Dcustom-terminfo-install-location` no longer accepts `no` as a special value, to disable exporting `TERMINFO`. To achieve the same result, simply don't set it at all. If it _is_ set, `TERMINFO` is still exported, like before. * The default install location for the terminfo definitions have been changed back to `${datadir}/terminfo`. * `dpi-aware=auto`: fonts are now scaled using the monitor's DPI only when **all** monitors have a scaling factor of one ([#714](https://codeberg.org/dnkl/foot/issues/714)). * fcft >= 3.0.0 in now required. ### Fixed * Added workaround for GNOME bug where multiple button press events (for the same button) is sent to the CSDs without any release or leave events in between ([#709](https://codeberg.org/dnkl/foot/issues/709)). * Line-wise selection not taking soft line-wrapping into account ([#726](https://codeberg.org/dnkl/foot/issues/726)). ### Contributors * [craigbarnes](https://codeberg.org/craigbarnes) * Arnavion ## 1.9.0 ### Added * Window title in the CSDs ([#638](https://codeberg.org/dnkl/foot/issues/638)). * `-Ddocs=disabled|enabled|auto` meson command line option. * Support for `~`-expansion in the `include` directive ([#659](https://codeberg.org/dnkl/foot/issues/659)). * Unicode 13 characters U+1FB3C - U+1FB6F, U+1FB9A and U+1FB9B to list of box drawing characters rendered by foot itself (rather than using font glyphs) ([#474](https://codeberg.org/dnkl/foot/issues/474)). * `XM`+`xm` to terminfo. * Mouse buttons 6/7 (mouse wheel left/right). * `url.uri-characters` option to `foot.ini` ([#654](https://codeberg.org/dnkl/foot/issues/654)). ### Changed * Terminfo files can now co-exist with the foot terminfo files from ncurses. See `INSTALL.md` for more information ([#671](https://codeberg.org/dnkl/foot/issues/671)). * `bold-text-in-bright=palette-based` now only brightens colors from palette * Raised grace period between closing the PTY and sending `SIGKILL` (when terminating the client application) from 4 to 60 seconds. * When terminating the client application, foot now sends `SIGTERM` immediately after closing the PTY, instead of waiting 2 seconds. * Foot now sends `SIGTERM`/`SIGKILL` to the client application's process group, instead of just to the client application's process. * `kmous` terminfo capability from `\E[M` to `\E[<`. * pt-or-px values (`letter-spacing`, etc) and the line thickness (`tweak.box-drawing-base-thickness`) in box drawing characters are now translated to pixel values using the monitor's scaling factor when `dpi-aware=no`, or `dpi-aware=auto` and the scaling factor is larger than 1 ([#680](https://codeberg.org/dnkl/foot/issues/680)). * Spawning a new terminal with a working directory that does not exist is no longer a fatal error. ### Removed * `km`/`smm`/`rmm` from terminfo; foot prefixes Alt-key combinations with `ESC`, and not by setting the 8:th "meta" bit, regardless of `smm`/`rmm`. While this _can_ be disabled by, resetting private mode 1036, the terminfo should reflect the **default** behavior ([#670](https://codeberg.org/dnkl/foot/issues/670)). * Keypad application mode keys from terminfo; enabling the keypad application mode is not enough to make foot emit these sequences - you also need to disable private mode 1035 ([#670](https://codeberg.org/dnkl/foot/issues/670)). ### Fixed * Rendering into the right margin area with `tweak.overflowing-glyphs` enabled. * PGO builds with clang ([#642](https://codeberg.org/dnkl/foot/issues/642)). * Crash in scrollback search mode when selection has been canceled due to terminal content updates ([#644](https://codeberg.org/dnkl/foot/issues/644)). * Foot process not terminating when the Wayland connection is broken ([#651](https://codeberg.org/dnkl/foot/issues/651)). * Output scale being zero on compositors that does not advertise a scaling factor. * Slow-to-terminate client applications causing other footclient instances to freeze when closing a footclient window. * Underlying cell content showing through in the left-most column of sixels. * `cursor.blink` not working in GNOME ([#686](https://codeberg.org/dnkl/foot/issues/686)). * Blinking cursor stops blinking, or becoming invisible, when switching focus from, and then back to a terminal window on GNOME ([#686](https://codeberg.org/dnkl/foot/issues/686)). ### Contributors * Nihal Jere * [nowrep](https://codeberg.org/nowrep) * [clktmr](https://codeberg.org/clktmr) ## 1.8.2 ### Added * `locked-title=no|yes` to `foot.ini` ([#386](https://codeberg.org/dnkl/foot/issues/386)). * `tweak.overflowing-glyphs` option, which can be enabled to fix rendering issues with glyphs of any width that appear cut-off ([#592](https://codeberg.org/dnkl/foot/issues/592)). ### Changed * Non-empty lines are now considered to have a hard linebreak, _unless_ an actual word-wrap is inserted. * Setting `DECSDM` now _disables_ sixel scrolling, while resetting it _enables_ scrolling ([#631](https://codeberg.org/dnkl/foot/issues/631)). ### Removed * The `tweak.allow-overflowing-double-width-glyphs` and `tweak.pua-double-width` options (which have been superseded by `tweak.overflowing-glyphs`). ### Fixed * FD exhaustion when repeatedly entering/exiting URL mode with many URLs. * Double free of URL while removing duplicated and/or overlapping URLs in URL mode ([#627](https://codeberg.org/dnkl/foot/issues/627)). * Crash when an unclosed OSC-8 URL ran into un-allocated scrollback rows. * Some box-drawing characters were rendered incorrectly on big-endian architectures. * Crash when resizing the window to the smallest possible size while scrollback search is active. * Scrollback indicator being incorrectly rendered when window size is very small. * Reduced memory usage in URL mode. * Crash when the `E3` escape (`\E[3J`) was executed, and there was a selection, or sixel image, in the scrollback ([#633](https://codeberg.org/dnkl/foot/issues/633)). ### Contributors * [clktmr](https://codeberg.org/clktmr) ## 1.8.1 ### Added * `--log-level=none` command-line option. * `Tc`, `setrgbf` and `setrgbb` capabilities in `foot` and `foot-direct` terminfo entries. This should make 24-bit RGB colors work in tmux and neovim, without the need for config hacks or detection heuristics ([#615](https://codeberg.org/dnkl/foot/issues/615)). ### Changed * Grapheme cluster width is now limited to two cells by default. This may cause cursor synchronization issues with many applications. You can set `[tweak].grapheme-width-method=wcswidth` to revert to the behavior in foot-1.8.0. ### Fixed * Grapheme cluster state being reset between codepoints. * Regression: custom URL key bindings not working ([#614](https://codeberg.org/dnkl/foot/issues/614)). ### Contributors * [craigbarnes](https://codeberg.org/craigbarnes) ## 1.8.0 ### Grapheme shaping This release adds _experimental, opt-in_ support for grapheme cluster segmentation and grapheme shaping. (note: several of the examples below may not render correctly in your browser, viewer or editor). Grapheme cluster segmentation is the art of splitting up text into grapheme clusters, where a cluster may consist of more than one Unicode codepoint. For example, 🙂 is a single codepoint, while 👩🏽‍🚀 consists of 4 codepoints (_Woman_ + _Medium skin tone_ + _Zero width joiner_ + _Rocket_). The goal is to _cluster_ codepoints belonging to the same grapheme in the same cell in the terminal. Previous versions of foot implemented a simple grapheme cluster segmentation technique that **only** handled zero-width codepoints. This allowed us to cluster combining characters, like q́ (_q_ + _COMBINING ACUTE ACCENT_). Once we have a grapheme cluster, we need to _shape_ it. Combining characters are simple: they are typically rendered as multiple glyphs layered on top of each other. This is why previous versions of foot got away with it without any actual text shaping support. Beyond that, support from the font library is needed. Foot now depends on fcft-2.4, which added support for grapheme and text shaping. When rendering a cell, we ask the font library: give us the glyph(s) for this sequence of codepoints. Fancy emoji sequences aside, using libutf8proc for grapheme cluster segmentation means **improved correctness**. For full support, the following is required: * fcft compiled with HarfBuzz support * foot compiled with libutf8proc support * `tweak.grapheme-shaping=yes` in `foot.ini` If `tweak.grapheme-shaping` has **not** been enabled, foot will neither use libutf8proc to do grapheme cluster segmentation, nor will it use fcft's grapheme shaping capabilities to shape combining characters. This feature is _experimental_ mostly due to the "wcwidth" problem; how many cells should foot allocate for a grapheme cluster? While the answer may seem simple, the problem is that, whatever the answer is, the client application **must** come up with the **same** answer. Otherwise we get cursor synchronization issues. In this release, foot simply adds together the `wcwidth()` of all codepoints in the grapheme cluster. This is equivalent to running `wcswidth()` on the entire cluster. **This is likely to change in the future**. Finally, note that grapheme shaping is not the same thing as text (or text run) shaping. In this version, foot only shapes individual graphemes, not entire text runs. That means e.g. ligatures are **not** supported. ### Added * Support for DECSET/DECRST 2026, as an alternative to the existing "synchronized updates" DCS sequences ([#459](https://codeberg.org/dnkl/foot/issues/459)). * `cursor.beam-thickness` option to `foot.ini` ([#464](https://codeberg.org/dnkl/foot/issues/464)). * `cursor.underline-thickness` option to `foot.ini` ([#524](https://codeberg.org/dnkl/foot/issues/524)). * Unicode 13 characters U+1FB70 - U+1FB8B to list of box drawing characters rendered by foot itself (rather than using font glyphs) ([#471](https://codeberg.org/dnkl/foot/issues/471)). * Dedicated `[bell]` section to config, supporting multiple actions and a new `command` action to run an arbitrary command. (https://codeberg.org/dnkl/foot/pulls/483) * Dedicated `[url]` section to config. * `[url].protocols` option to `foot.ini` ([#531](https://codeberg.org/dnkl/foot/issues/531)). * Support for setting the full 256 color palette in foot.ini ([#489](https://codeberg.org/dnkl/foot/issues/489)) * XDG activation support, will be used by `[bell].urgent` when available (falling back to coloring the window margins red when unavailable) ([#487](https://codeberg.org/dnkl/foot/issues/487)). * `ctrl`+`c` as a default key binding; to cancel search/url mode. * `${window-title}` to `notify`. * Support for including files in `foot.ini` ([#555](https://codeberg.org/dnkl/foot/issues/555)). * `ENVIRONMENT` section in **foot**(1) and **footclient**(1) man pages ([#556](https://codeberg.org/dnkl/foot/issues/556)). * `tweak.pua-double-width` option to `foot.ini`, letting you force _Private Usage Area_ codepoints to be treated as double-width characters. * OSC 9 desktop notifications (iTerm2 compatible). * Support for LS2 and LS3 (locking shift) escape sequences ([#581](https://codeberg.org/dnkl/foot/issues/581)). * Support for overriding configuration options on the command line ([#554](https://codeberg.org/dnkl/foot/issues/554), [#600](https://codeberg.org/dnkl/foot/issues/600)). * `underline-offset` option to `foot.ini` ([#490](https://codeberg.org/dnkl/foot/issues/490)). * `csd.button-color` option to `foot.ini`. * `-Dterminfo-install-location=disabled|` meson command line option ([#569](https://codeberg.org/dnkl/foot/issues/569)). ### Changed * [fcft](https://codeberg.org/dnkl/fcft): required version bumped from 2.3.x to 2.4.x. * `generate-alt-random-writes.py --sixel`: width and height of emitted sixels has been adjusted. * _Concealed_ text (`\E[8m`) is now revealed when highlighted. * The background color of highlighted text is now adjusted, when the foreground and background colors are the same, making the highlighted text legible ([#455](https://codeberg.org/dnkl/foot/issues/455)). * `cursor.style=bar` to `cursor.style=beam`. `bar` remains a recognized value, but will eventually be deprecated, and removed. * Point values in `line-height`, `letter-spacing`, `horizontal-letter-offset` and `vertical-letter-offset` are now rounded, not truncated, when translated to pixel values. * Foot's exit code is now -26/230 when foot itself failed to launch (due to invalid command line options, client application/shell not found etc). Footclient's exit code is -36/220 when it itself fails to launch (e.g. bad command line option) and -26/230 when the foot server failed to instantiate a new window ([#466](https://codeberg.org/dnkl/foot/issues/466)). * Background alpha no longer applied to palette or RGB colors that matches the background color. * Improved performance on compositors that does not release shm buffers immediately, e.g. KWin ([#478](https://codeberg.org/dnkl/foot/issues/478)). * `ctrl + w` (_extend-to-word-boundary_) can now be used across lines ([#421](https://codeberg.org/dnkl/foot/issues/421)). * Ignore auto-detected URLs that overlap with OSC-8 URLs. * Default value for the `notify` option to use `-a ${app-id} -i ${app-id} ...` instead of `-a foot -i foot ...`. * `scrollback-*`+`pipe-scrollback` key bindings are now passed through to the client application when the alt screen is active ([#573](https://codeberg.org/dnkl/foot/issues/573)). * Reverse video (`\E[?5h`) now only swaps the default foreground and background colors. Cells with explicit foreground and/or background colors remain unchanged. * Tabs (`\t`) are now preserved when the window is resized, and when copying text ([#508](https://codeberg.org/dnkl/foot/issues/508)). * Writing a sixel on top of another sixel no longer erases the first sixel, but the two are instead blended ([#562](https://codeberg.org/dnkl/foot/issues/562)). * Running foot without a configuration file is no longer an error; it has been demoted to a warning, and is no longer presented as a notification in the terminal window, but only logged on stderr. ### Deprecated * `bell` option in `foot.ini`; set actions in the `[bell]` section instead. * `url-launch` option in `foot.ini`; use `launch` in the `[url]` section instead. * `jump-label-letters` option in `foot.ini`; use `label-letters` in the `[url]` section instead. * `osc8-underline` option in `foot.ini`; use `osc8-underline` in the `[url]` section instead. ### Removed * Buffer damage quirk for Plasma/KWin. ### Fixed * `generate-alt-random-writes.py --sixel` sometimes crashing, resulting in PGO build failures. * Wrong colors in the 256-color cube ([#479](https://codeberg.org/dnkl/foot/issues/479)). * Memory leak triggered by "opening" an OSC-8 URI and then resetting the terminal without closing the URI ([#495](https://codeberg.org/dnkl/foot/issues/495)). * Assertion when emitting a sixel occupying the entire scrollback history ([#494](https://codeberg.org/dnkl/foot/issues/494)). * Font underlines being positioned below the cell (and thus being invisible) for certain combinations of fonts and font sizes ([#503](https://codeberg.org/dnkl/foot/issues/503)). * Sixels with transparent bottom border being resized below the size specified in _"Set Raster Attributes"_. * Fonts sometimes not being reloaded with the correct scaling factor when `dpi-aware=no`, or `dpi-aware=auto` with monitor(s) with a scaling factor > 1 ([#509](https://codeberg.org/dnkl/foot/issues/509)). * Crash caused by certain CSI sequences with very large parameter values ([#522](https://codeberg.org/dnkl/foot/issues/522)). * Rare occurrences where the window did not close when the shell exited. Only seen on FreeBSD ([#534](https://codeberg.org/dnkl/foot/issues/534)) * Foot process(es) sometimes remaining, using 100% CPU, when closing multiple foot windows at the same time ([#542](https://codeberg.org/dnkl/foot/issues/542)). * Regression where `+shift+tab` always produced `\E[Z` instead of the correct `\E[27;;9~` sequence ([#547](https://codeberg.org/dnkl/foot/issues/547)). * Crash when a line wrapping OSC-8 URI crossed the scrollback wrap around ([#552](https://codeberg.org/dnkl/foot/issues/552)). * Selection incorrectly wrapping rows ending with an explicit newline ([#565](https://codeberg.org/dnkl/foot/issues/565)). * Off-by-one error in markup of auto-detected URLs when the URL ends in the right-most column. * Multi-column characters being cut in half when resizing the alternate screen. * Restore `SIGHUP` in spawned processes. * Text reflow performance ([#504](https://codeberg.org/dnkl/foot/issues/504)). * IL+DL (`CSI Ps L` + `CSI Ps M`) now moves the cursor to column 0. * SS2 and SS3 (single shift) escape sequences behaving like locking shifts ([#580](https://codeberg.org/dnkl/foot/issues/580)). * `TEXT`+`STRING`+`UTF8_STRING` mime types not being recognized in clipboard offers ([#583](https://codeberg.org/dnkl/foot/issues/583)). * Memory leak caused by custom box drawing glyphs not being completely freed when destroying a foot window instance ([#586](https://codeberg.org/dnkl/foot/issues/586)). * Crash in scrollback search when current XKB layout is missing _compose_ definitions. * Window title not being updated while window is hidden ([#591](https://codeberg.org/dnkl/foot/issues/591)). * Crash on badly formatted URIs in e.g. OSC-8 URLs. * Window being incorrectly resized on CSD/SSD run-time changes. ### Contributors * [r\_c\_f](https://codeberg.org/r_c_f) * [craigbarnes](https://codeberg.org/craigbarnes) ## 1.7.2 ### Added * URxvt OSC-11 extension to set background alpha ([#436](https://codeberg.org/dnkl/foot/issues/436)). * OSC 17/117/19/119 - change/reset selection background/foreground color. * `box-drawings-uses-font-glyphs=yes|no` option to `foot.ini` ([#430](https://codeberg.org/dnkl/foot/issues/430)). ### Changed * Underline cursor is now rendered below text underline ([#415](https://codeberg.org/dnkl/foot/issues/415)). * Foot now tries much harder to keep URL jump labels inside the window geometry ([#443](https://codeberg.org/dnkl/foot/issues/443)). * `bold-text-in-bright` may now be set to `palette-based`, in which case it will use the corresponding bright palette color when the color to brighten matches one of the base 8 colors, instead of increasing the luminance ([#449](https://codeberg.org/dnkl/foot/issues/449)). ### Fixed * Reverted _"Consumed modifiers are no longer sent to the client application"_ ([#425](https://codeberg.org/dnkl/foot/issues/425)). * Crash caused by a double free originating in `XTSMGRAPHICS` - set number of color registers ([#427](https://codeberg.org/dnkl/foot/issues/427)). * Wrong action referenced in error message for key binding collisions ([#432](https://codeberg.org/dnkl/foot/issues/432)). * OSC 4/104 out-of-bounds accesses to the color table. This was the reason pywal turned foot windows transparent ([#434](https://codeberg.org/dnkl/foot/issues/434)). * PTY not being drained when the client application terminates. * `auto_left_margin` not being limited to `cub1` ([#441](https://codeberg.org/dnkl/foot/issues/441)). * Crash in scrollback search mode when searching beyond the last output. ### Contributors * [cglogic](https://codeberg.org/cglogic) ## 1.7.1 ### Changed * Update PGO build instructions in `INSTALL.md` ([#418](https://codeberg.org/dnkl/foot/issues/418)). * In scrollback search mode, empty cells can now be matched by spaces. ### Fixed * Logic that repairs invalid key bindings ended up breaking valid key bindings instead ([#407](https://codeberg.org/dnkl/foot/issues/407)). * Custom `line-height` settings now scale when increasing or decreasing the font size at run-time. * Newlines sometimes incorrectly inserted into copied text ([#410](https://codeberg.org/dnkl/foot/issues/410)). * Crash when compositor send `text-input-v3::enter` events without first having sent a `keyboard::enter` event ([#411](https://codeberg.org/dnkl/foot/issues/411)). * Deadlock when rendering sixel images. * URL labels, scrollback search box or scrollback position indicator sometimes not showing up, caused by invalidly sized surface buffers when output scaling was enabled ([#409](https://codeberg.org/dnkl/foot/issues/409)). * Empty sixels resulted in non-empty images. ## 1.7.0 ### Added * The `pad` option now accepts an optional third argument, `center` (e.g. `pad=5x5 center`), causing the grid to be centered in the window, with equal amount of padding of the left/right and top/bottom side ([#273](https://codeberg.org/dnkl/foot/issues/273)). * `line-height`, `letter-spacing`, `horizontal-letter-offset` and `vertical-letter-offset` to `foot.ini`. These options let you tweak cell size and glyph positioning ([#244](https://codeberg.org/dnkl/foot/issues/244)). * Key/mouse binding `select-extend-character-wise`, which forces the selection mode to 'character-wise' when extending a selection. * `DECSET` `47`, `1047` and `1048`. * URL detection and OSC-8 support. URLs are highlighted and activated using the keyboard (**no** mouse support). See **foot**(1)::URLs, or [README.md](README.md#urls) for details ([#14](https://codeberg.org/dnkl/foot/issues/14)). * `-d,--log-level={info|warning|error}` to both `foot` and `footclient` ([#337](https://codeberg.org/dnkl/foot/issues/337)). * `-D,--working-directory=DIR` to both `foot` and `footclient` ([#347](https://codeberg.org/dnkl/foot/issues/347)) * `DECSET 80` - sixel scrolling ([#361](https://codeberg.org/dnkl/foot/issues/361)). * `DECSET 1070` - sixel private color palette ([#362](https://codeberg.org/dnkl/foot/issues/362)). * `DECSET 8452` - position cursor to the right of sixels ([#363](https://codeberg.org/dnkl/foot/issues/363)). * Man page **foot-ctlseqs**(7), documenting all supported escape sequences ([#235](https://codeberg.org/dnkl/foot/issues/235)). * Support for transparent sixels (DCS parameter `P2=1`) ([#391](https://codeberg.org/dnkl/foot/issues/391)). * `-N,--no-wait` to `footclient` ([#395](https://codeberg.org/dnkl/foot/issues/395)). * Completions for Bash shell ([#10](https://codeberg.org/dnkl/foot/issues/10)). * Implement `XTVERSION` (`CSI > 0q`). Foot will reply with `DCS>|foot(..)ST` ([#359](https://codeberg.org/dnkl/foot/issues/359)). ### Changed * The fcft and tllist library subprojects are now handled via Meson [wrap files](https://mesonbuild.com/Wrap-dependency-system-manual.html) instead of needing to be manually cloned. * Box drawing characters are now rendered by foot, instead of using font glyphs ([#198](https://codeberg.org/dnkl/foot/issues/198)) * Double- or triple clicking then dragging now extends the selection word- or line-wise ([#267](https://codeberg.org/dnkl/foot/issues/267)). * The line thickness of box drawing characters now depend on the font size ([#281](https://codeberg.org/dnkl/foot/issues/281)). * Extending a word/line-wise selection now uses the original selection mode instead of switching to character-wise. * While doing an interactive resize of a foot window, foot now requires 100ms of idle time (where the window size does not change) before sending the new dimensions to the client application. The timing can be tweaked, or completely disabled, by setting `resize-delay-ms` ([#301](https://codeberg.org/dnkl/foot/issues/301)). * `CSI 13 ; 2 t` now reports (0,0). * Key binding matching logic; key combinations like `Control+Shift+C` **must** now be written as either `Control+C` or `Control+Shift+c`, the latter being the preferred variant. ([#376](https://codeberg.org/dnkl/foot/issues/376)) * Consumed modifiers are no longer sent to the client application ([#376](https://codeberg.org/dnkl/foot/issues/376)). * The minimum version requirement for the libxkbcommon dependency is now 1.0.0. * Empty pixel rows at the bottom of a sixel is now trimmed. * Sixels with DCS parameter `P2=0|2` now use the _current_ ANSI background color for empty pixels instead of the default background color ([#391](https://codeberg.org/dnkl/foot/issues/391)). * Sixel decoding optimized; up to 100% faster in some cases. * Reported sixel "max geometry" from current window size, to the configured maximum size (defaulting to 10000x10000). ### Removed * The `-g,--geometry` command-line option (which had been deprecated and superseded by `-w,--window-size-pixels` since 1.5.0). ### Fixed * Some mouse bindings (_primary paste_, for example) did not require `shift` to be pressed while used in a mouse grabbing application. This meant the mouse event was never seen by the application. * Terminals spawned with `ctrl`+`shift`+`n` not terminating when exiting shell ([#366](https://codeberg.org/dnkl/foot/issues/366)). * Default value of `-t,--term` in `--help` output when foot was built without terminfo support. * Drain PTY when the client application terminates. ### Contributors * [craigbarnes](https://codeberg.org/craigbarnes) * toast * [l3mon4d3](https://codeberg.org/l3mon4d3) * [Simon Schricker](mailto:s.schricker@sillage.at) ## 1.6.4 ### Added * `selection-target=none|primary|clipboard|both` to `foot.ini`. It can be used to configure which clipboard(s) selected text should be copied to. The default is `primary`, which corresponds to the behavior in older foot releases ([#288](https://codeberg.org/dnkl/foot/issues/288)). ### Changed * The IME state no longer stays stuck in the terminal if the IME goes away during preedit. * `-Dterminfo` changed from a `boolean` to a `feature` option. * Use standard signals instead of a signalfd to handle `SIGCHLD`. Fixes an issue on FreeBSD where foot did not detect when the client application had terminated. ### Fixed * `BS`, `HT` and `DEL` from being stripped in bracketed paste mode. ### Contributors * [tdeo](https://codeberg.org/tdeo) * jbeich ## 1.6.3 ### Added * Completions for fish shell ([#11](https://codeberg.org/dnkl/foot/issues/11)) * FreeBSD support ([#238](https://codeberg.org/dnkl/foot/issues/238)). * IME popup location support: foot now sends the location of the cursor so any popup can be displayed near the text that is being typed. ### Changed * Trailing comments in `foot.ini` must now be preceded by a space or tab ([#270](https://codeberg.org/dnkl/foot/issues/270)) * The scrollback search box no longer accepts non-printable characters. * Non-formatting C0 control characters, `BS`, `HT` and `DEL` are now stripped from pasted text. ### Fixed * Exit when the client application terminates, not when the TTY file descriptor is closed. * Crash on compositors not implementing the _text input_ interface ([#259](https://codeberg.org/dnkl/foot/issues/259)). * Erased, overflowing glyphs (when `tweak.allow-overflowing-double-width-glyphs=yes` - the default) not properly erasing the cell overflowed **into**. * `word-delimiters` option ignores `#` and subsequent characters ([#270](https://codeberg.org/dnkl/foot/issues/270)) * Combining characters not being rendered when composed with colored bitmap glyphs (i.e. colored emojis). * Pasting URIs from the clipboard when the source has not newline-terminated the last URI ([#291](https://codeberg.org/dnkl/foot/issues/291)). * Sixel "current geometry" query response not being bounded by the current window dimensions (fixes `lsix` output) * Crash on keyboard input when repeat rate was zero (i.e. no repeat). * Wrong button encoding of mouse buttons 6 and 7 in mouse events. * Scrollback search not matching composed characters. * High CPU usage when holding down e.g. arrow keys while in scrollback search mode. * Rendering of composed characters in the scrollback search box. * IME pre-edit cursor when positioned at the end of the pre-edit string. * Scrollback search not matching multi-column characters. ### Contributors * [pc](https://codeberg.org/pc) * [FollieHiyuki](https://codeberg.org/FollieHiyuki) * jbeich * [tdeo](https://codeberg.org/tdeo) ## 1.6.2 ### Fixed * Version number in `meson.build`. ## 1.6.1 ### Added * `--seed` to `generate-alt-random.py`, enabling deterministic PGO builds. ### Changed * Use `-std=c11` instead of `-std=c18`. * Added `-Wno-profile-instr-unprofiled` to Clang cflags in PGO builds ([INSTALL.md](https://codeberg.org/dnkl/foot/src/branch/releases/1.6/INSTALL.md#user-content-performance-optimized-pgo)) ### Fixed * Missing dependencies in meson, causing heavily parallelized builds to fail. * Background color when alpha < 1.0 being wrong ([#249](https://codeberg.org/dnkl/foot/issues/249)). * `generate-alt-random.py` failing in containers. ### Contributors * [craigbarnes](https://codeberg.org/craigbarnes) * [sterni](https://codeberg.org/sterni) ## 1.6.0 ### For packagers Starting with this release, foot can be PGO:d (compiled using profile guided optimizations) **without** a running Wayland session. This means foot can be PGO:d in e.g. sandboxed build scripts. See [INSTALL.md](INSTALL.md#user-content-performance-optimized-pgo). ### Added * IME support. This is compile-time optional, see [INSTALL.md](INSTALL.md#user-content-options) ([#134](https://codeberg.org/dnkl/foot/issues/134)). * `DECSET` escape to enable/disable IME: `CSI ? 737769 h` enables IME and `CSI ? 737769 l` disables it. This can be used to e.g. enable/disable IME when entering/leaving insert mode in vim. * `dpi-aware` option to `foot.ini`. The default, `auto`, sizes fonts using the monitor's DPI when output scaling has been **disabled**. If output scaling has been **enabled**, fonts are sized using the scaling factor. DPI-only font sizing can be forced by setting `dpi-aware=yes`. Setting `dpi-aware=no` forces font sizing to be based on the scaling factor. ([#206](https://codeberg.org/dnkl/foot/issues/206)). * Implement reverse auto-wrap (_auto\_left\_margin_, _bw_, in terminfo). This mode can be enabled/disabled with `CSI ? 45 h` and `CSI ? 45 l`. It is **enabled** by default ([#150](https://codeberg.org/dnkl/foot/issues/150)). * `bell` option to `foot.ini`. Can be set to `set-urgency` to make foot render the margins in red when receiving `BEL` while **not** having keyboard focus. Applications can dynamically enable/disable this with the `CSI ? 1042 h` and `CSI ? 1042 l` escape sequences. Note that Wayland does **not** implement an _urgency_ hint like X11, but that there is a [proposal](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/9) to add support for this. The value `set-urgency` was chosen for forward-compatibility, in the hopes that this proposal eventualizes ([#157](https://codeberg.org/dnkl/foot/issues/157)). * `bell` option can also be set to `notify`, in which case a desktop notification is emitted when foot receives `BEL` in an unfocused window. * `word-delimiters` option to `foot.ini` ([#156](https://codeberg.org/dnkl/foot/issues/156)). * `csd.preferred` can now be set to `none` to disable window decorations. Note that some compositors will render SSDs despite this option being used ([#163](https://codeberg.org/dnkl/foot/issues/163)). * Terminal content is now auto-scrolled when moving the mouse above or below the window while selecting ([#149](https://codeberg.org/dnkl/foot/issues/149)). * `font-bold`, `font-italic` `font-bold-italic` options to `foot.ini`. These options allow custom bold/italic fonts. They are unset by default, meaning the bold/italic version of the regular font is used ([#169](https://codeberg.org/dnkl/foot/issues/169)). * Drag & drop support; text, files and URLs can now be dropped in a foot terminal window ([#175](https://codeberg.org/dnkl/foot/issues/175)). * `clipboard-paste` and `primary-paste` scrollback search bindings. By default, they are bound to `ctrl+v ctrl+y` and `shift+insert` respectively, and lets you paste from the clipboard or primary selection into the search buffer. * Support for `pipe-*` actions in mouse bindings. It was previously not possible to add a command to these actions when used in mouse bindings, making them useless ([#183](https://codeberg.org/dnkl/foot/issues/183)). * `bold-text-in-bright` option to `foot.ini`. When enabled, bold text is rendered in a brighter color ([#199](https://codeberg.org/dnkl/foot/issues/199)). * `-w,--window-size-pixels` and `-W,--window-size-chars` command line options to `footclient` ([#189](https://codeberg.org/dnkl/foot/issues/189)). * Short command line options for `--title`, `--maximized`, `--fullscreen`, `--login-shell`, `--hold` and `--check-config`. * `DECSET` escape to modify the `escape` key to send `\E[27;1;27~` instead of `\E`: `CSI ? 27127 h` enables the new behavior, `CSI ? 27127 l` disables it (the default). * OSC 777;notify: desktop notifications. Use in combination with the new `notify` option in `foot.ini` ([#224](https://codeberg.org/dnkl/foot/issues/224)). * Status line terminfo capabilities `hs`, `tsl`, `fsl` and `dsl`. This enables e.g. vim to set the window title ([#242](https://codeberg.org/dnkl/foot/issues/242)). ### Changed * Blinking text now uses the foreground color, but dimmed down in its off state, instead of the background color. * Sixel default maximum size is now 10000x10000 instead of the current window size. * Graphical glitches/flashes when resizing the window while running a fullscreen application, i.e. the 'alt' screen ([#221](https://codeberg.org/dnkl/foot/issues/221)). * Cursor will now blink if **either** `CSI ? 12 h` or `CSI Ps SP q` has been used to enable blinking. **cursor.blink** in `foot.ini` controls the default state of `CSI Ps SP q` ([#218](https://codeberg.org/dnkl/foot/issues/218)). * The sub-parameter versions of the SGR RGB color escapes (e.g `\E[38:2...m`) can now be used _without_ the color space ID parameter. * SGR 21 no longer disables **bold**. According to ECMA-48, SGR 21 is _"double underline_". Foot does not (yet) implement that, but that's no reason to implement a non-standard behavior. * `DECRQM` now returns actual state of the requested mode, instead of always returning `2`. ### Removed * Support for loading configuration from `$XDG_CONFIG_HOME/footrc`. * `scrollback` option from `foot.ini`. * `geometry` from `foot.ini`. * Key binding action `scrollback-up` and `scrollback-down`. ### Fixed * Error when re-assigning a default key binding ([#233](https://codeberg.org/dnkl/foot/issues/233)). * `\E[s`+`\E[u` (save/restore cursor) now saves and restores attributes and charset configuration, just like `\E7`+`\E8`. * Report mouse motion events to the client application also while dragging the cursor outside the grid. * Parsing of the sub-parameter versions of indexed SGR color escapes (e.g. `\E[38:5...m`) * Frames occasionally being rendered while application synchronized updates is in effect. * Handling of failures to parse the font specification string. * Extra private/intermediate characters in escape sequences not being ignored. ### Contributors * [kennylevinsen](https://codeberg.org/kennylevinsen) * [craigbarnes](https://codeberg.org/craigbarnes) ## 1.5.4 ### Changed * Num Lock by default overrides the keypad mode. See **foot.ini**(5)::KEYPAD, or [README.md](README.md#user-content-keypad) for details ([#194](https://codeberg.org/dnkl/foot/issues/194)). * Single-width characters with double-width glyphs are now allowed to overflow into neighboring cells by default. Set **tweak.allow-overflowing-double-width-glyphs** to 'no' to disable this. ### Fixed * Resize very slow when window is hidden ([#190](https://codeberg.org/dnkl/foot/issues/190)). * Key mappings for key combinations with `shift`+`tab` ([#210](https://codeberg.org/dnkl/foot/issues/210)). * Key mappings for key combinations with `alt`+`return`. * `footclient` `-m` (`--maximized`) flag being ignored. * Crash with explicitly sized sixels with a height less than 6 pixels. * Key mappings for `esc` with modifiers. ### Contributors * [craigbarnes](https://codeberg.org/craigbarnes) ## 1.5.3 ### Fixed * Crash when libxkbcommon cannot find a suitable libX11 _compose_ file. Note that foot will run, but without support for dead keys. ([#170](https://codeberg.org/dnkl/foot/issues/170)). * Restored window size when window is un-tiled. * XCursor shape in CSD corners when window is tiled. * Error handling when processing keyboard input (maybe [#171](https://codeberg.org/dnkl/foot/issues/171)). * Compilation error _"overflow in conversion from long 'unsigned int' to 'int' changes value... "_ seen on platforms where the `request` argument in `ioctl(3)` is an `int` (for example: linux/ppc64). * Crash when using the mouse in alternate scroll mode in an unfocused window ([#179](https://codeberg.org/dnkl/foot/issues/179)). * Character dropped from selection when "right-click-hold"-extending a selection ([#180](https://codeberg.org/dnkl/foot/issues/180)). ## 1.5.2 ### Fixed * Regression: middle clicking double pastes in e.g. vim ([#168](https://codeberg.org/dnkl/foot/issues/168)) ## 1.5.1 ### Changed * Default value of the **scrollback.multiplier** option in `foot.ini` from `1.0` to `3.0`. * `shift`+`insert` now pastes from the primary selection by default. This is in addition to middle-clicking with the mouse. ### Fixed * Mouse bindings now match even if the actual click count is larger than specified in the binding. This allows you to, for example, quickly press the middle-button to paste multiple times ([#146](https://codeberg.org/dnkl/foot/issues/146)). * Color flashes when changing the color palette with OSC 4,10,11 ([#141](https://codeberg.org/dnkl/foot/issues/141)). * Scrollback position is now retained when resizing the window ([#142](https://codeberg.org/dnkl/foot/issues/142)). * Trackpad scrolling speed to better match the mouse scrolling speed, and to be consistent with other (Wayland) terminal emulators. Note that it is (much) slower compared to previous foot versions. Use the **scrollback.multiplier** option in `foot.ini` if you find the new speed too slow ([#144](https://codeberg.org/dnkl/foot/issues/144)). * Crash when `foot.ini` contains an invalid section name ([#159](https://codeberg.org/dnkl/foot/issues/159)). * Background opacity when in _reverse video_ mode. * Crash when writing a sixel image that extends outside the terminal's right margin ([#151](https://codeberg.org/dnkl/foot/issues/151)). * Sixel image at non-zero column positions getting sheared at seemingly random occasions ([#151](https://codeberg.org/dnkl/foot/issues/151)). * Crash after either resizing a window or changing the font size if there were sixels present in the scrollback while doing so. * _Send Device Attributes_ to only send a response if `Ps == 0`. * Paste from primary when clipboard is empty. ### Contributors * [craigbarnes](https://codeberg.org/craigbarnes) * [zar](https://codeberg.org/zar) ## 1.5.0 ### Deprecated * `$XDG_CONFIG_HOME/footrc`/`~/.config/footrc`. Use `$XDG_CONFIG_HOME/foot/foot.ini`/`~/.config/foot/foot.ini` instead. * **scrollback** option in `foot.ini`. Use **scrollback.lines** instead. * **scrollback-up** key binding. Use **scrollback-up-page** instead. * **scrollback-down** key binding. Use **scrollback-down-page** instead. ### Added * Scrollback position indicator. This feature is optional and controlled by the **scrollback.indicator-position** and **scrollback.indicator-format** options in `foot.ini` ([#42](https://codeberg.org/dnkl/foot/issues/42)). * Key bindings in _scrollback search_ mode are now configurable. * `--check-config` command line option. * **pipe-selected** key binding. Works like **pipe-visible** and **pipe-scrollback**, but only pipes the currently selected text, if any ([#51](https://codeberg.org/dnkl/foot/issues/51)). * **mouse.hide-when-typing** option to `foot.ini`. * **scrollback.multiplier** option to `foot.ini` ([#54](https://codeberg.org/dnkl/foot/issues/54)). * **colors.selection-foreground** and **colors.selection-background** options to `foot.ini`. * **tweak.render-timer** option to `foot.ini`. * Modifier support in mouse bindings ([#77](https://codeberg.org/dnkl/foot/issues/77)). * Click count support in mouse bindings, i.e double- and triple-click ([#78](https://codeberg.org/dnkl/foot/issues/78)). * All mouse actions (begin selection, select word, select row etc) are now configurable, via the new **select-begin**, **select-begin-block**, **select-extend**, **select-word**, **select-word-whitespace** and **select-row** options in the **mouse-bindings** section in `foot.ini` ([#79](https://codeberg.org/dnkl/foot/issues/79)). * Implement XTSAVE/XTRESTORE escape sequences, `CSI ? Ps s` and `CSI ? Ps r` ([#91](https://codeberg.org/dnkl/foot/issues/91)). * `$COLORTERM` is now set to `truecolor` at startup, to indicate support for 24-bit RGB colors. * Experimental support for rendering double-width glyphs with a character width of 1. Must be explicitly enabled with `tweak.allow-overflowing-double-width-glyphs` ([#116](https://codeberg.org/dnkl/foot/issues/116)). * **initial-window-size-pixels** options to `foot.ini` and `-w,--window-size-pixels` command line option to `foot`. This option replaces the now deprecated **geometry** and `-g,--geometry` options. * **initial-window-size-chars** option to `foot.ini` and `-W,--window-size-chars` command line option to `foot`. This option configures the initial window size in **characters**, and is an alternative to **initial-window-size-pixels**. * **scrollback-up-half-page** and **scrollback-down-half-page** key bindings. They scroll up/down half of a page in the scrollback ([#128](https://codeberg.org/dnkl/foot/issues/128)). * **scrollback-up-line** and **scrollback-down-line** key bindings. They scroll up/down a single line in the scrollback. * **mouse.alternate-scroll-mode** option to `foot.ini`. This option controls the initial state of the _Alternate Scroll Mode_, and defaults to `yes`. When enabled, mouse scroll events are translated to up/down key events in the alternate screen, letting you scroll in e.g. `less` and other applications without enabling native mouse support in them ([#135](https://codeberg.org/dnkl/foot/issues/135)). ### Changed * Renamed man page for `foot.ini` from **foot**(5) to **foot.ini**(5). * Configuration errors are no longer fatal; foot will start and print an error inside the terminal (and of course still log errors on stderr). * Default `--server` socket path to use `$WAYLAND_DISPLAY` instead of `$XDG_SESSION_ID` ([#55](https://codeberg.org/dnkl/foot/issues/55)). * Trailing empty cells are no longer highlighted in mouse selections. * Foot now searches for its configuration in `$XDG_DATA_DIRS/foot/foot.ini`, if no configuration is found in `$XDG_CONFIG_HOME/foot/foot.ini` or in `$XDG_CONFIG_HOME/footrc`. * Minimum window size changed from four rows and 20 columns, to 1 row and 2 columns. * **scrollback-up/down** renamed to **scrollback-up/down-page**. * fcft >= 2.3.0 is now required. ### Fixed * Command lines for **pipe-visible** and **pipe-scrollback** are now tokenized (i.e. syntax checked) when the configuration is loaded, instead of every time the key binding is executed. * Incorrect multi-column character spacer insertion when reflowing text. * Compilation errors in 32-bit builds. * Mouse cursor style in top and left margins. * Selection is now **updated** when the cursor moves outside the grid ([#70](https://codeberg.org/dnkl/foot/issues/70)). * Viewport sometimes not moving when doing a scrollback search. * Crash when canceling a scrollback search and the window had been resized while searching. * Selection start point not moving when the selection changes direction. * OSC 10/11/104/110/111 (modify colors) did not update existing screen content ([#94](https://codeberg.org/dnkl/foot/issues/94)). * Extra newlines when copying empty cells ([#97](https://codeberg.org/dnkl/foot/issues/97)). * Mouse events from being sent to client application when a mouse binding has consumed it. * Input events from getting mixed with paste data ([#101](https://codeberg.org/dnkl/foot/issues/101)). * Missing DPI values for "some" monitors on Gnome ([#118](https://codeberg.org/dnkl/foot/issues/118)). * Handling of multi-column composed characters while reflowing. * Escape sequences sent for key combinations with `Return`, that did **not** include `Alt`. * Clipboard (or primary selection) is now cleared when receiving an OSC-52 command with an invalid base64 encoded payload. * Cursor position being set outside the grid when reflowing text. * CSD buttons to be hidden when window size becomes so small that they no longer fit. ### Contributors * [craigbarnes](https://codeberg.org/craigbarnes) * [birger](https://codeberg.org/birger) * [Ordoviz](https://codeberg.org/Ordoviz) * [cherti](https://codeberg.org/cherti) ## 1.4.4 ### Changed * Mouse cursor is now always a `left_ptr` when inside the margins, to indicate it is not possible to start a selection. ### Fixed * Crash when starting a selection inside the margins. * Improved font size consistency across multiple monitors with different DPI ([#47](https://codeberg.org/dnkl/foot/issues/47)). * Handle trailing comments in `footrc` ## 1.4.3 ### Added * Section to [README.md](README.md) describing how to programmatically identify foot. * [LICENSE](LICENSE), [README.md](README.md) and [CHANGELOG.md](CHANGELOG.md) are now installed to `${datadir}/doc/foot`. * Support for escaping quotes in **pipe-visible** and **pipe-scrollback** commands. ### Changed * Primary DA to no longer indicate support for _Selective Erase_, _Technical Characters_ and _Terminal State Interrogation_. * Secondary DA to report foot as a VT220 instead of a VT420. * Secondary DA to report foot's version number in parameter 2, the _Firmware Version_. The string is made up of foot's major, minor and patch version numbers, always using two digits for each version number and without any other separating characters. Thus, _1.4.2_ would be reported as `010402` (i.e. the full response would be `\E[>1;010402;0c`). * Scrollback search to only move the viewport if the match lies outside it. * Scrollback search to focus match, that requires a viewport change, roughly in the center of the screen. * Extending a selection with the right mouse button now works while dragging the mouse. ### Fixed * Crash in scrollback search. * Crash when a **pipe-visible** or **pipe-scrollback** command contained an unclosed quote ([#49](https://codeberg.org/dnkl/foot/issues/49)). ### Contributors * [birger](https://codeberg.org/birger) * [cherti](https://codeberg.org/cherti) ## 1.4.2 ### Changed * Maximum window title length from 100 to 2048. ### Fixed * Crash when overwriting a sixel and the row being overwritten did not cover an entire cell. * Assertion failure in debug builds when overwriting a sixel image. ## 1.4.1 ### Fixed * Compilation errors in release builds with some combinations of compilers and compiler flags. ## 1.4.0 ### Added * `Sync` to terminfo. This is a tmux extension that indicates _"Synchronized Updates"_ are supported. * `--hold` command line option to `footclient`. * Key mapping for `KP_Decimal`. * Terminfo entries for keypad keys: `ka1`, `ka2`, `ka3`, `kb1`, `kb3`, `kc1`, `kc2`, `kc3`, `kp5`, `kpADD`, `kpCMA`, `kpDIV`, `kpDOT`, `kpMUL`, `kpSUB` and `kpZRO`. * **blink** option to `footrc`; a boolean that lets you control whether the cursor should blink or not by default. Note that applications can override this. * Multi-seat support * Implemented `C0::FF` (form feed) * **pipe-visible** and **pipe-scrollback** key bindings. These let you pipe either the currently visible text, or the entire scrollback to external tools ([#29](https://codeberg.org/dnkl/foot/issues/29)). Example: `pipe-visible=[sh -c "xurls | bemenu | xargs -r firefox] Control+Print` ### Changed * Background transparency to only be used with the default background color. * Copy-to-clipboard/primary-selection to insert a line break if either the last cell on the previous line or the first cell on the next line is empty. * Number of lines to scroll is now always clamped to the number of lines in the scrolling region.. * New terminal windows spawned with `ctrl`+`shift`+`n` are no longer double forked. * Unicode combining character overflow errors are only logged when debug logging has been enabled. * OSC 4 (_Set Color_) now updates already rendered cells, excluding scrollback. * Mouse cursor from `hand2` to `left_ptr` when client is capturing the mouse. * Sixel images are now removed when the font size is **decreased**. * `DECSCUSR` (_Set Cursor Style_, `CSI Ps SP q`) now uses `Ps=0` instead of `Ps=2` to reset the style to the user configured default style. `Ps=2` now always configures a _Steady Block_ cursor. * `Se` terminfo capability from `\E[2 q` to `\E[ q`. * Hollow cursor to be drawn when window has lost _keyboard_ focus rather than _visual_ focus. ### Fixed * Do not stop an ongoing selection when `shift` is released. When the client application is capturing the mouse, one must hold down `shift` to start a selection. This selection is now finalized only when the mouse button is released - not as soon as `shift` is released. * Selected cells did not appear selected if programmatically modified. * Rare crash when scrolling and the new viewport ended up **exactly** on the wrap around. * Selection handling when viewport wrapped around. * Restore signal mask in the client process. * Set `IUTF8`. * Selection of double-width characters. It is no longer possible to select half of a double-width character. * Draw hollow block cursor on top of character. * Set an initial `TIOCSWINSZ`. This ensures clients never read a `0x0` terminal size ([#20](https://codeberg.org/dnkl/foot/issues/20)). * Glyphs overflowing into surrounding cells ([#21](https://codeberg.org/dnkl/foot/issues/21)). * Crash when last rendered cursor cell had scrolled off screen and `\E[J3` was executed. * Assert (debug builds) when an `\e]4` OSC escape was not followed by a `;`. * Window title always being set to "foot" on reset. * Terminfo entry `kb2` (center keypad key); it is now set to `\EOu` (which is what foot emits) instead of the incorrect value `\EOE`. * Palette reuse in sixel images. Previously, the palette was reset after each image. * Do not auto-resize a sixel image for which the client has specified a size. This fixes an issue where an image would incorrectly overflow into the cell row beneath. * Text printed, or other sixel images drawn, on top of a sixel image no longer erases the entire image, only the part(s) covered by the new text or image. * Sixel images being erased when printing text next to them. * Sixel handling when resizing window. * Sixel handling when scrollback wraps around. * Foot now issues much fewer `wl_surface_damage_buffer()` calls ([#35](https://codeberg.org/dnkl/foot/issues/35)). * `C0::VT` to be processed as `C0::LF`. Previously, `C0::VT` would only move the cursor down, but never scroll. * `C0::HT` (_Horizontal Tab_, or `\t`) no longer clears `LCF` (_Last Column Flag_). * `C0::LF` now always clears `LCF`. Previously, it only cleared it when the cursor was **not** at the bottom of the scrolling region. * `IND` and `RI` now clears `LCF`. * `DECAWM` now clears `LCF`. * A multi-column character that does not fit on the current line is now printed on the next line, instead of only printing half the character. * Font size can no longer be reduced to negative values ([#38](https://codeberg.org/dnkl/foot/issues/38)). ## 1.3.0 ### Added * User configurable key- and mouse bindings. See `man 5 foot` and the example `footrc` ([#1](https://codeberg.org/dnkl/foot/issues/1)) * **initial-window-mode** option to `footrc`, that lets you control the initial mode for each newly spawned window: _windowed_, _maximized_ or _fullscreen_. * **app-id** option to `footrc` and `--app-id` command line option, that sets the _app-id_ property on the Wayland window. * **title** option to `footrc` and `--title` command line option, that sets the initial window title. * Right mouse button extends the current selection. * `CSI Ps ; Ps ; Ps t` escape sequences for the following parameters: `11t`, `13t`, `13;2t`, `14t`, `14;2t`, `15t`, `19t`. * Unicode combining characters. ### Changed * Spaces no longer removed from zsh font name completions. * Default key binding for _spawn-terminal_ to ctrl+shift+n. * Renderer is now much faster with interactive scrolling ([#4](https://codeberg.org/dnkl/foot/issues/4)) * memfd sealing failures are no longer fatal errors. * Selection to no longer be cleared on resize. * The current monitor's subpixel order (RGB/BGR/V-RGB/V-BGR) is preferred over FontConfig's `rgba` property. Only if the monitor's subpixel order is `unknown` is FontConfig's `rgba` property used. If the subpixel order is `none`, then grayscale antialiasing is used. The subpixel order is ignored if antialiasing has been disabled. * The four primary font variants (normal, bold, italic, bold italic) are now loaded in parallel. This speeds up both the initial startup time, as well as DPI changes. * Command line parsing no longer tries to parse arguments following the command-to-execute. This means one can now write `foot sh -c true` instead of `foot -- sh -c true`. ### Removed * Keyboard/pointer handler workarounds for Sway 1.2. ### Fixed * Sixel images moved or deleted on window resize. * Cursor sometimes incorrectly restored on exit from alternate screen. * 'Underline' cursor being invisible on underlined text. * Restored cursor position in 'normal' screen when window was resized while in 'alt' screen. * Hostname in OSC 7 URI not being validated. * OSC 4 with multiple `c;spec` pairs. * Alt+Return to emit "ESC \r". * Trackpad sloooow scrolling to eventually scroll a line. * Memory leak in terminal reset. * Translation of cursor coordinates on resize * Scaling color specifiers in OSC sequences. * `OSC 12 ?` to return the cursor color, not the cursor's text color. * `OSC 12;#000000` to configure the cursor to use inverted foreground/background colors. * Call `ioctl(TIOCSCTTY)` on the pts fd in the slave process. ## 1.2.3 ### Fixed * Forgot to version bump 1.2.2 ## 1.2.2 ### Changed * Changed icon name in `foot.desktop` and `foot-server.desktop` from _terminal_ to _utilities-terminal_. * `XDG_SESSION_ID` is now included in the server/daemon default socket path. ### Fixed * Window size doubling when moving window between outputs with different scaling factors ([#3](https://codeberg.org/dnkl/foot/issues/3)). * Font being too small on monitors with fractional scaling ([#5](https://codeberg.org/dnkl/foot/issues/5)). ## 1.2.1 ### Fixed * Building AUR package ## 1.2.0 ### Added * Run-time text resize using ctrl-+, ctrl+- and ctrl+0 * Font size adjusts dynamically to outputs' DPI * Reflow text when resizing window * **pad** option to `footrc` * **login-shell** option to `footrc` and `--login-shell` command line option * Client side decorations (CSDs). This finally makes foot usable on GNOME. * Sixel graphics support * OSC 12 and 112 escape sequences (set/reset text cursor color) * REP CSI escape sequence * `oc` to terminfo * foot-server.desktop file * Window and cell size reporting escape sequences * `--hold` command line option * `--print-pid=FILE|FD` command line option ### Changed * Subpixel antialiasing is only enabled when background is opaque * Meta/alt ESC prefix can now be disabled with `\E[?1036l`. In this mode, the 8:th bit is set and the result is UTF-8 encoded. This can also be disabled with `\E[1024l` (in which case the Alt key is effectively being ignored). * terminfo now uses ST instead of BEL as OSC terminator * Logging to print to stderr, not stdout * Backspace now emits DEL (^?), and ctrl+backspace emits BS (^H) ### Removed * '28' from DA response foot-1.21.0/CODE_OF_CONDUCT.md000066400000000000000000000067741476600145200153640ustar00rootroot00000000000000# 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 truely 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. foot-1.21.0/INSTALL.md000066400000000000000000000411111476600145200141750ustar00rootroot00000000000000# Installing 1. [Overview](#overview) 1. [Requirements](#requirements) 1. [Running](#running) 1. [Building](#building) 1. [Other](#other) 1. [Setup](#setup) 1. [Options](#options) 1. [Release build](#release-build) 1. [Size optimized](#size-optimized) 1. [Performance optimized, non-PGO](#performance-optimized-non-pgo) 1. [Performance optimized, PGO](#performance-optimized-pgo) 1. [Partial PGO](#partial-pgo) 1. [Full PGO](#full-pgo) 1. [Use the generated PGO data](#use-the-generated-pgo-data) 1. [Profile Guided Optimization](#profile-guided-optimization) 1. [Debug build](#debug-build) 1. [Terminfo](#terminfo) 1. [Running the new build](#running-the-new-build) ## Overview foot makes use of a couple of libraries I have developed: [tllist](https://codeberg.org/dnkl/tllist) and [fcft](https://codeberg.org/dnkl/fcft). As such, they will most likely not have been installed already. You can either install them as system libraries or build them as _subprojects_ in foot. When building foot, they will first be searched for as system libraries. If **found**, foot will link dynamically against them. If **not** found, meson will attempt to download and build them as subprojects. ## Requirements ### Running * UTF-8 locale * fontconfig * freetype * pixman * wayland (_client_ and _cursor_ libraries) * xkbcommon * utf8proc (_optional_, needed for grapheme clustering) * libutempter (_optional_, needed for utmp logging on Linux) * ulog (_optional_, needed for utmp logging on FreeBSD) * [fcft](https://codeberg.org/dnkl/fcft) [^1] [^1]: can also be built as subprojects, in which case they are statically linked. If you are packaging foot, you may also want to consider adding the following **optional** dependencies: * libnotify: desktop notifications by default uses `notify-send`. * xdg-utils: URLs are by default launched with `xdg-open`. * bash-completion: If you want completion for positional arguments. ### Building In addition to the dev variant of the packages above, you need: * meson * ninja * wayland protocols * ncurses (needed to generate terminfo) * scdoc (for man page generation, not needed if documentation is disabled) * llvm (for PGO builds with Clang) * [tllist](https://codeberg.org/dnkl/tllist) [^1] * systemd (optional, foot will install systemd unit files if detected) A note on compilers; in general, foot runs **much** faster when compiled with gcc instead of clang. A profile-guided gcc build can be more than twice as fast as a clang build. **Note** GCC 10.1 has a performance regression that severely affects foot when doing PGO builds and building with `-O2`; it is about 30-40% slower compared to GCC 9.3. The work around is simple: make sure you build with `-O3`. This is the default with `meson --buildtype=release`, but e.g. `makepkg` can override it (`makepkg` uses `-O2` by default). ## Other Foot uses _meson_. If you are unfamiliar with it, the official [tutorial](https://mesonbuild.com/Tutorial.html) might be a good starting point. A note on terminfo; the terminfo database exposes terminal capabilities to the applications running inside the terminal. As such, it is important that the terminfo used reflects the actual terminal. Using the `xterm-256color` terminfo will, in many cases, work, but I still recommend using foot's own terminfo. There are two reasons for this: * foot's terminfo contains a couple of non-standard capabilities, used by e.g. tmux. * New capabilities added to the `xterm-256color` terminfo could potentially break foot. * There may be future additions or changes to foot's terminfo. As of ncurses 2021-07-31, ncurses includes a version of foot's terminfo. **The recommendation is to use those**, and only install the terminfo definitions from this git repo if the system's ncurses predates 2021-07-31. But, note that the foot terminfo definitions in ncurses' lack the non-standard capabilities. This mostly affects tmux; without them, `terminal-overrides` must be configured to enable truecolor support. For this reason, it _is_ possible to install "our" terminfo definitions as well, either in a non-default location, or under a different name. Both have their set of issues. When installing to a non-default location, foot will set the environment variable `TERMINFO` in the child process. However, there are many situations where this simply does not work. See https://codeberg.org/dnkl/foot/issues/695 for details. Installing them under a different name generally works well, but will break applications that check if `$TERM == foot`. Hence the recommendation to simply use ncurses' terminfo definitions if available. If packaging "our" terminfo definitions, I recommend doing that as a separate package, to allow them to be installed on remote systems without having to install foot itself. ### Setup To build, first, create a build directory, and switch to it: ```sh mkdir -p bld/release && cd bld/release ``` ### Options Available compile-time options: | Option | Type | Default | Description | Extra dependencies | |--------------------------------------|---------|-------------------------|---------------------------------------------------------------------------------|---------------------| | `-Ddocs` | feature | `auto` | Builds and install documentation | scdoc | | `-Dtests` | bool | `true` | Build tests (adds a `ninja test` build target) | None | | `-Dime` | bool | `true` | Enables IME support | None | | `-Dgrapheme-clustering` | feature | `auto` | Enables grapheme clustering | libutf8proc | | `-Dterminfo` | feature | `enabled` | Build and install terminfo files | tic (ncurses) | | `-Ddefault-terminfo` | string | `foot` | Default value of `TERM` | None | | `-Dterminfo-base-name` | string | `-Ddefault-terminfo` | Base name of the generated terminfo files | None | | `-Dcustom-terminfo-install-location` | string | `${datadir}/terminfo` | Value to set `TERMINFO` to | None | | `-Dsystemd-units-dir` | string | `${systemduserunitdir}` | Where to install the systemd service files (absolute) | None | | `-Dutmp-backend` | combo | `auto` | Which utmp backend to use (`none`, `libutempter`, `ulog` or `auto`) | libutempter or ulog | | `-Dutmp-default-helper-path` | string | `auto` | Default path to utmp helper binary. `auto` selects path based on `utmp-backend` | None | Documentation includes the man pages, readme, changelog and license files. `-Ddefault-terminfo`: I strongly recommend leaving the default value. Use this option if you plan on installing the terminfo files under a different name. Setting this changes the default value of `$TERM`, and the names of the terminfo files (if `-Dterminfo=enabled`). If you want foot to use the terminfo files from ncurses, but still package foot's own terminfo files under a different name, you can use the `-Dterminfo-base-name` option. Many distributions use the name `foot-extra`, and thus it might be a good idea to reuse that: ```sh meson ... -Ddefault-terminfo=foot -Dterminfo-base-name=foot-extra ``` (or just leave out `-Ddefault-terminfo`, since it defaults to `foot` anyway). Finally, `-Dcustom-terminfo-install-location` enables foot's terminfo to co-exist with ncurses' version, without changing the terminfo names. The idea is that you install foot's terminfo to a non-standard location, for example `/usr/share/foot/terminfo`. Use `-Dcustom-terminfo-install-location` to tell foot where the terminfo is. Foot will set the environment variable `TERMINFO` to this value (with `${prefix}` added). The value is **relative to ${prefix}**. Note that there are several issues with this approach: https://codeberg.org/dnkl/foot/issues/695. If left unset, foot will **not** set or modify `TERMINFO`. `-Dterminfo` can be used to disable building the terminfo definitions in the meson build. It does **not** change the default value of `TERM`, and it does **not** disable `TERMINFO`, if `-Dcustom-terminfo-install-location` has been set. Use this if packaging the terminfo definitions in a separate package (and the build script isn't shared with the 'foot' package). Example: ```sh meson --prefix=/usr -Dcustom-terminfo-install-location=lib/foot/terminfo ``` The above tells foot its terminfo definitions will be installed to `/usr/lib/foot/terminfo`. This is the value foot will set the `TERMINFO` environment variable to. If `-Dterminfo` is enabled (the default), then the terminfo files will be built as part of the regular build process, and installed to the specified location. Packagers may want to set `-Dterminfo=disabled`, and manually build and [install the terminfo](#terminfo) files instead. ### Release build Below are instructions for building foot either [size optimized](#size-optimized), [performance optimized](performance-optimized-non-pgo), or performance optimized using [PGO](#performance-optimized-pgo). PGO - _Profile Guided Optimization_ - is a way to optimize a program better than `-O3` can, and is done by compiling foot twice: first to generate an instrumented version which is used to run a payload that exercises the performance critical parts of foot, and then a second time to rebuild foot using the generated profiling data to guide optimization. In addition to being faster, PGO builds also tend to be smaller than regular `-O3` builds. #### Size optimized To optimize for size (i.e. produce a small binary): ```sh export CFLAGS="$CFLAGS -Os" meson --buildtype=release --prefix=/usr -Db_lto=true ../.. ninja ninja test ninja install ``` #### Performance optimized, non-PGO To do a regular, non-PGO build optimized for performance: ```sh export CFLAGS="$CFLAGS -O3" meson --buildtype=release --prefix=/usr -Db_lto=true ../.. ninja ninja test ninja install ``` Use `-O2` instead of `-O3` if you prefer a slightly smaller (and slower!) binary. #### Performance optimized, PGO There are a lot more steps involved in a PGO build, and for this reason there are a number of helper scripts available. `pgo/pgo.sh` is a standalone script that pieces together the other scripts in the `pgo` directory to do a complete PGO build. This script is intended to be used when doing manual builds. Note that all "full" PGO builds (which `auto` will prefer, if possible) **require** `LC_CTYPE` to be set to an UTF-8 locale. This is **not** done automatically. Example: ```sh cd foot ./pgo/pgo.sh auto . /tmp/foot-pgo-build-output ``` (run `./pgo/pgo.sh` to get help on usage) It supports a couple of different PGO builds; partial (covered in detail below), full (also covered in detail below), and (full) headless builds using Sway or cage. Packagers may want to use it as inspiration, but may choose to support only a specific build type; e.g. full/headless with Sway. To do a manual PGO build, instead of using the script(s) mentioned above, detailed instructions follows: First, configure the build directory: ```sh export CFLAGS="$CFLAGS -O3" meson --buildtype=release --prefix=/usr -Db_lto=true ../.. ``` It is **very** important `-O3` is being used here, as GCC-10.1.x and later have a regression where PGO with `-O2` is **much** slower. Clang users **must** add `-Wno-ignored-optimization-argument` to `CFLAGS`. Then, tell meson we want to _generate_ profiling data, and build: ```sh meson configure -Db_pgo=generate ninja ninja test ``` Next, we need to actually generate the profiling data. There are two ways to do this: a [partial PGO build using a PGO helper](#partial-pgo) binary, or a [full PGO build](#full-pgo) by running the real foot binary. The latter has slightly better results (i.e. results in a faster binary), but must be run in a Wayland session. A full PGO build also tends to be smaller than a partial build. ##### Partial PGO This method uses a PGO helper binary that links against the VT parser only. It is similar to a mock test; it instantiates a dummy terminal instance and then directly calls the VT parser with stimuli. It explicitly does **not** include the Wayland backend and as such, it does not require a running Wayland session. The downside is that not all code paths in foot is exercised. In particular, the **rendering** code is not. As a result, the final binary built using this method is slightly slower than when doing a [full PGO](#full-pgo) build. We will use the `pgo` binary along with input corpus generated by `scripts/generate-alt-random-writes.py`: ```sh ./utils/xtgettcap ./footclient --version ./foot --version tmp_file=$(mktemp) ../../scripts/generate-alt-random-writes \ --rows=67 \ --cols=135 \ --scroll \ --scroll-region \ --colors-regular \ --colors-bright \ --colors-256 \ --colors-rgb \ --attr-bold \ --attr-italic \ --attr-underline \ --sixel \ ${tmp_file} ./pgo ${tmp_file} ${tmp_file} ${tmp_file} rm ${tmp_file} ``` The first step, running `./foot --version` and `./footclient --version` etc, might seem unnecessary, but is needed to ensure we have _some_ profiling data for functions not covered by the PGO helper binary, for **all** binaries. Without this, the final link phase will fail. The snippet above then creates an (empty) temporary file. Then, it runs a script that generates random escape sequences (if you cat `${tmp_file}` in a terminal, you'll see random colored characters all over the screen). Finally, we feed the randomly generated escape sequences to the PGO helper. This is what generates the profiling data used in the next step. You are now ready to [use the generated PGO data](#use-the-generated-pgo-data). ##### Full PGO This method requires a running Wayland session. We will use the script `scripts/generate-alt-random-writes.py`: ```sh ./utils/xtgettcap ./footclient --version foot_tmp_file=$(mktemp) ./foot \ --config=/dev/null \ --override tweak.grapheme-shaping=no \ --term=xterm \ sh -c " --scroll --scroll-region --colors-regular --colors-bright --colors-256 --colors-rgb --attr-bold --attr-italic --attr-underline --sixel ${foot_tmp_file} && cat ${foot_tmp_file}" rm ${foot_tmp_file} ``` You should see a foot window open up, with random colored text. The window should close after ~1-2s. The first step, `./utils/xtgettcap && ./footclient --version` might seem unnecessary, but is needed to ensure we have _some_ profiling data for **all** binaries we build. Without this, the final link phase will fail. ##### Use the generated PGO data Now that we have _generated_ PGO data, we need to rebuild foot. This time telling meson (and ultimately gcc/clang) to _use_ the PGO data. If using Clang, now do (this requires _llvm_ to have been installed): ```sh llvm-profdata merge default_*profraw --output=default.profdata ``` Next, tell meson to _use_ the profile data we just generated, and rebuild: ```sh meson configure -Db_pgo=use ninja ninja test ``` Continue reading in [Running the new build](#running-the-new-build) ### Debug build ```sh meson --buildtype=debug ../.. ninja ninja test ``` ### Terminfo By default, building foot also builds the terminfo files. If packaging the terminfo files in a separate package, it might be easier to simply disable the terminfo files in the regular build, and compile the terminfo files manually instead. To build the terminfo files, run: ```sh sed 's/@default_terminfo@/foot/g' foot.info | \ tic -o -x -e foot,foot-direct - ``` Where _"output-directory"_ **must** match the value passed to `-Dcustom-terminfo-install-location` in the foot build. If `-Dcustom-terminfo-install-location` has not been set, `-o ` can simply be omitted. Or, if packaging: ```sh tic -o ${DESTDIR}/usr/share/terminfo ... ``` ### Running the new build You can now run it directly from the build directory: ```sh ./foot ``` Or, if you did not install the terminfo definitions: ```sh ./foot --term xterm-256color ``` foot-1.21.0/LICENSE000066400000000000000000000020561476600145200135570ustar00rootroot00000000000000MIT License Copyright (c) 2019 Daniel Eklöf Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. foot-1.21.0/README.md000066400000000000000000000563221476600145200140360ustar00rootroot00000000000000# ![Logo: a terminal with a foot shaped prompt](icons/hicolor/48x48/apps/foot.png) foot 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) [![Packaging status](https://repology.org/badge/vertical-allrepos/foot.svg?columns=4)](https://repology.org/project/foot/versions) ## Index 1. [Features](#features) 1. [Installing](#installing) 1. [Configuration](#configuration) 1. [Troubleshooting](#troubleshooting) 1. [Why the name 'foot'?](#why-the-name-foot) 1. [Fonts](#fonts) 1. [Shortcuts](#shortcuts) 1. [Keyboard](#keyboard) 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) 1. [DPI and font size](#dpi-and-font-size) 1. [Supported OSCs](#supported-oscs) 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) 1. [Mastodon](#mastodon) 1. [Sponsoring/donations](#sponsoring-donations) 1. [License](#license) ## Features * Fast (see [benchmarks](doc/benchmark.md), and [performance](https://codeberg.org/dnkl/foot/wiki/Performance)) * Lightweight, in dependencies, on-disk and in-memory * Wayland native * DE agnostic * Server/daemon mode * User configurable font fallback * On-the-fly font resize * On-the-fly DPI font size adjustment * Scrollback search * Keyboard driven URL detection * Color emoji support * 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") # Installing See [INSTALL.md](INSTALL.md). ## Configuration **foot** can be configured by creating a file `$XDG_CONFIG_HOME/foot/foot.ini` (defaulting to `~/.config/foot/foot.ini`). A template for that can usually be found in `/etc/xdg/foot/foot.ini` or [here](https://codeberg.org/dnkl/foot/src/branch/master/foot.ini). Further information can be found in foot's man page `foot.ini(5)`. ## Troubleshooting See the [wiki](https://codeberg.org/dnkl/foot/wiki#user-content-troubleshooting) ## Why the name 'foot'? I'm bad at names. Most of my projects usually start out as _foo something_ (for example, [yambar](https://codeberg.org/dnkl/yambar) was _f00bar_ for a while). So why _foot_? _foo terminal_ → _footerm_ → _foot_ Pretty bad, I know. As a side note, if you pronounce the _foo_ part of _foot_ the same way you pronounce _foobar_, then _foot_ sounds a lot like the Swedish word _fot_, which incidentally means (you guessed it) _foot_. ## Fonts **foot** supports all fonts that can be loaded by _freetype_, including **bitmap** fonts and **color emoji** fonts. Foot uses _fontconfig_ to locate and configure the font(s) to use. Since fontconfig's fallback mechanism is imperfect, especially for monospace fonts (it doesn't prefer monospace fonts even though the requested font is one), foot allows you, the user, to configure the fallback fonts to use. This also means you can configure _each_ fallback font individually; you want _that_ fallback font to use _this_ size, and you want that _other_ fallback font to be _italic_? No problem! If a glyph cannot be found in _any_ of the user configured fallback fonts, _then_ fontconfig's list is used. ## Shortcuts These are the default shortcuts. See `man foot.ini` and the example `foot.ini` to see how these can be changed. ### Keyboard #### Normal mode shift+page up/page down : Scroll up/down in history ctrl+shift+c, XF86Copy : Copy selected text to the _clipboard_ ctrl+shift+v, XF86Paste : Paste from _clipboard_ shift+insert : Paste from the _primary selection_ ctrl+shift+r : Start a scrollback search ctrl++, ctrl+= : Increase font size ctrl+- : Decrease font size ctrl+0 : Reset font size ctrl+shift+n : Spawn a new terminal. If the shell has been [configured to emit the OSC 7 escape sequence](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+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). ctrl+shift+x : Jump to the next prompt. Requires [shell integration](https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts). #### Scrollback search ctrl+r : Search _backward_ for next match ctrl+s : Search _forward_ for next match ctrl+w : Extend current selection (and thus the search criteria) to the end of the word, or the next word if currently at a word separating character. ctrl+shift+w : Same as ctrl+w, except that the only word separating characters are whitespace characters. ctrl+v, ctrl+shift+v, ctrl+y, XF86Paste : Paste from clipboard into the search buffer. shift+insert : Paste from primary selection into the search buffer. escape, ctrl+g : Cancel the search return : Finish the search and copy the current match to the primary selection ### URL mode t : Toggle whether the URL is displayed in the jump label or not escape, ctrl+c, ctrl+g, ctrl+d : Exit URL mode without launching any URLs ### Mouse left - **single-click** : Drag to select; when released, the selected text is copied to the _primary_ selection. This feature is **disabled** when client has enabled _mouse tracking_. : Holding shift enables selection in mouse tracking enabled clients. : Holding ctrl will create a block selection. left - **double-click** : Selects the _word_ (separated by spaces, period, comma, parenthesis etc) under the pointer. Hold ctrl to select everything under the pointer up to, and until, the next space characters. left - **triple-click** : Selects the everything between enclosing quotes, or the entire row if not inside a quote. left - **quad-click** : Selects the entire row. middle : Paste from _primary_ selection right : Extend current selection. Clicking immediately extends the selection, while hold-and-drag allows you to interactively resize the selection. ctrl+right : Extend the current selection, but force it to be character wise, rather than depending on the original selection mode. wheel : Scroll up/down in history ctrl+wheel : Increase/decrease font size ### Touchscreen tap : Emulates mouse left button click. drag : Scrolls up/down in history. : Holding for a while before dragging (time delay can be configured) emulates mouse dragging with left button held. ## Server (daemon) mode When run normally, **foot** is a single-window application; if you want another window, start another foot process. However, foot can also be run in a _server_ mode. In this mode, one process hosts multiple windows. All Wayland communication, VT parsing and rendering is done in the server process. New windows are opened by running `footclient`, which remains running until the terminal window is closed, at which point it exits with the exit value of the client process (typically the shell). The point of this mode is **a)** reduced memory footprint - all terminal windows will share fonts and glyph cache, and **b)** reduced startup time - loading fonts and populating the glyph cache takes time, but in server mode it only happens once. The downside is a performance penalty; all windows' input and output are multiplexed in the same thread (but each window will have its own set of rendering threads). This means that if one window is very busy with, for example, producing output, then other windows will suffer. And of course, should the server process crash, **all** windows will be gone. Typical usage would be to start the server process (`foot --server`) 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 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). ## URLs Foot supports URL detection. But, unlike many other terminal emulators, where URLs are highlighted when they are hovered and opened by clicking on them, foot uses a keyboard driven approach. Pressing ctrl+shift+o enters _"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 `foot.ini` man page. `show-urls-launch` by default opens the URL with `xdg-open`. This can be changed with the `url-launch` option. `show-urls-copy` is an alternative to `show-urls-launch`, that changes what activating a URL _does_; instead of opening it, it copies it to the clipboard. It is unbound by default. Jump label colors, the URL underline color, and the letters used in the jump label key sequences can be configured. ## Shell integration ### Current working directory New foot terminal instances (bound to ctrl+shift+n by default) will open in the current working directory, **if** the shell in the "parent" terminal reports directory changes. This is done with the OSC-7 escape sequence. Most shells can be scripted to do this, if they do not support it natively. See the [wiki](https://codeberg.org/dnkl/foot/wiki#user-content-spawning-new-terminal-instances-in-the-current-working-directory) for details. ### Jumping between prompts Foot can move the current viewport to focus prompts of already executed commands (bound to ctrl+shift+z/x by default). For this to work, the shell needs to emit an OSC-133;A (`\E]133;A\E\\`) sequence before each prompt. In zsh, one way to do this is to add a `precmd` hook: ```zsh precmd() { print -Pn "\e]133;A\e\\" } ``` See the [wiki](https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts) for details, and examples for other shells. ### Piping last command's output The key binding `pipe-command-output` can pipe the last command's output to an application of your choice (similar to the other `pipe-*` key bindings): ```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 By default, foot prefixes _Meta characters_ with ESC. This corresponds to XTerm's `metaSendsEscape` option set to `true`. This can be disabled programmatically with `\E[?1036l` (and enabled again with `\E[?1036h`). When disabled, foot will instead set the 8:th bit of meta character and then UTF-8 encode it. This corresponds to XTerm's `eightBitMeta` option set to `true`. This can also be disabled programmatically with `rmm` (_reset meta mode_, `\E[?1034l`), and enabled again with `smm` (_set meta mode_, `\E[?1034h`). ## Backspace Foot transmits DEL (`^?`) on backspace. This corresponds to XTerm's `backarrowKey` option set to `false`, and to [`DECBKM`](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 described above **cannot** be changed. Finally, pressing alt will prefix the transmitted byte with ESC. ## Keypad By default, Num Lock overrides the run-time configuration keypad mode; when active, the keypad is always considered to be in _numerical_ mode. This corresponds to XTerm's `numLock` option set to `true`. In this mode, the keypad keys always sends either numbers (Num Lock is **active**) or cursor movement keys (Up, Down, Left, Right, Page Up, Page Down etc). This can be disabled programmatically with `\E[?1035l` (and enabled again with `\E[?1035h`). When disabled, the keypad sends custom escape sequences instead of numbers, when in _application_ mode. ## DPI and font size Font sizes are apparently a complex thing. Many applications use a fixed DPI of 96. They may also multiply it with the monitor's scale factor. This results in fonts with different **physical** sizes (i.e. if measured by a ruler) when rendered on screens with different DPI values. Even if the configured font size is the same. 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. 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. 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. 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. 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 with the terminal emulator itself. Foot implements the following OSCs: * `OSC 0` - change window icon + title (but only title is actually supported) * `OSC 2` - change window title * `OSC 4` - change color palette * `OSC 7` - report CWD (see [shell integration](#shell-integration)) * `OSC 8` - hyperlink * `OSC 9` - desktop notification * `OSC 10` - change (default) foreground color * `OSC 11` - change (default) background color * `OSC 12` - change cursor color * `OSC 17` - change highlight (selection) background color * `OSC 19` - change highlight (selection) foreground color * `OSC 22` - set the xcursor (mouse) pointer * `OSC 52` - copy/paste clipboard data * `OSC 104` - reset color palette * `OSC 110` - reset default foreground color * `OSC 111` - reset default background color * `OSC 112` - reset cursor color * `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-ctlseqs**(7) man page for a complete list of supported control sequences. ## Programmatically checking if running in foot Foot does **not** set any environment variables that can be used to identify foot (reading `TERM` is not reliable since the user may have chosen to use a different terminfo). You can instead use the escape sequences to read the _Secondary_ and _Tertiary Device Attributes_ (secondary/tertiary DA, for short). The tertiary DA response is always `\EP!|464f4f54\E\\`, where `464f4f54` is `FOOT` in hex. The secondary DA response is `\E[>1;XXYYZZ;0c`, where `XXYYZZ` is foot's major, minor and patch version numbers, in decimal, using two digits for each number. For example, foot-1.4.2 would respond with `\E[>1;010402;0c`. **Note**: not all terminal emulators implement tertiary DA. Most implement secondary DA, but not all. All _should_ however implement _Primary DA_. Thus, a safe way to query the terminal is to request the tertiary, secondary and primary DA all at once, in that order. All terminals should ignore escape sequences they do not recognize. You will have to parse the response (which in foot will consist of all three DA responses, all at once) to determine which requests the terminal 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 build. # XTGETTCAP `XTGETTCAP` is an escape sequence initially introduced by XTerm, and also implemented (and extended, to some degree) by Kitty. It allows querying the terminal for terminfo 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. XTerm's implementation (as of XTerm-370) only supports querying key (as in keyboard keys) capabilities, and three custom capabilities: * `TN` - terminal name * `Co` - number of colors (alias for the `colors` capability) * `RGB` - number of bits per color channel (different semantics from the `RGB` capability in file-based terminfo definitions!). Kitty has extended this, and also supports querying all integer and string capabilities. Foot supports this, and extends it even further, to also include boolean capabilities. This means foot's entire terminfo can be queried via `XTGETTCAP`. Note that both Kitty and foot handles **responses** to multi-capability queries slightly differently, compared to XTerm. XTerm will send a single DCS reply, with `;`-separated capability/value pairs. There are a couple of issues with this: * The success/fail flag in the beginning of the response is always `1` (success), unless the very **first** queried capability is invalid. * XTerm will not respond **at all** to an invalid capability, unless it's the first one in the `XTGETTCAP` query. * XTerm will end the response at the first invalid capability. In other words, if you send a large multi-capability query, you will only get responses up to, but not including, the first invalid capability. All subsequent capabilities will be dropped. Kitty and foot on the other hand, send one DCS response for **each** capability in the multi query. This allows us to send a proper success/fail flag for each queried capability. Responses for **all** queried capabilities are **always** sent. No queries are ever dropped. All replies are in `tigetstr()` format. That is, given the same capability name, foot's reply is identical to what `tigetstr()` would have returned. # Credits * [Ordoviz](https://codeberg.org/Ordoviz), for designing and 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 Before you open a new issue, please search existing bug reports, both open **and** closed ones. Chances are someone else has already reported the same issue. The report should contain the following: - Foot version (`foot --version`). - Log output from foot (run `foot -d info` from another terminal). - Which Wayland compositor (and version) you are running. - If reporting a crash, please try to provide a `bt full` backtrace with symbols. - Steps to reproduce. The more details the better. # Contact ## IRC Ask questions, hang out, sing praise or just say hi in the `#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@social.treehouse.systems](https://social.treehouse.systems/@dnkl) # Sponsoring/donations * GitHub Sponsors: https://github.com/sponsors/dnkl # License Foot is released under the [MIT license](LICENSE). foot-1.21.0/async.c000066400000000000000000000013501476600145200140270ustar00rootroot00000000000000#include "async.h" #include #include #include #define LOG_MODULE "async" #define LOG_ENABLE_DBG 0 #include "log.h" enum async_write_status async_write(int fd, const void *_data, size_t len, size_t *idx) { const uint8_t *const data = _data; size_t left = len - *idx; while (left > 0) { ssize_t ret = write(fd, &data[*idx], left); if (ret < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) return ASYNC_WRITE_REMAIN; return ASYNC_WRITE_ERR; } LOG_DBG("wrote %zd bytes of %zu (%zu left) to FD=%d", ret, left, left - ret, fd); *idx += ret; left -= ret; } return ASYNC_WRITE_DONE; } foot-1.21.0/async.h000066400000000000000000000014031476600145200140330ustar00rootroot00000000000000#pragma once #include enum async_write_status {ASYNC_WRITE_DONE, ASYNC_WRITE_REMAIN, ASYNC_WRITE_ERR}; /* * Primitive that writes data to a NONBLOCK:ing FD. * * _data: points to the beginning of the buffer * len: total size of the data buffer * idx: pointer to byte offset into data buffer - writing starts here. * * Thus, the total amount of data to write is (len - *idx). *idx is * updated such that it points to the next unwritten byte in the data * buffer. * * I.e. if the return value is: * - ASYNC_WRITE_DONE, then the *idx == len. * - ASYNC_WRITE_REMAIN, then *idx < len * - ASYNC_WRITE_ERR, there was an error, and no data was written */ enum async_write_status async_write( int fd, const void *data, size_t len, size_t *idx); foot-1.21.0/base64.c000066400000000000000000000107551476600145200140070ustar00rootroot00000000000000#include "base64.h" #include #include #include #include #include #define LOG_MODULE "base64" #define LOG_ENABLE_DBG 0 #include "log.h" #include "debug.h" enum { P = 1 << 6, // Padding byte (=) I = 1 << 7, // Invalid byte ([^A-Za-z0-9+/=]) }; static const uint8_t reverse_lookup[256] = { I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, 62, I, I, I, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, I, I, I, P, I, I, I, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, I, I, I, I, I, I, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I }; static const char lookup[64] = { "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789+/" }; char * base64_decode(const char *s, size_t *size) { const size_t len = strlen(s); if (unlikely(len % 4 != 0)) { errno = EINVAL; return NULL; } char *ret = malloc(len / 4 * 3 + 1); if (unlikely(ret == NULL)) return NULL; if (unlikely(size != NULL)) *size = len / 4 * 3; for (size_t i = 0, o = 0; i < len; i += 4, o += 3) { unsigned a = reverse_lookup[(unsigned char)s[i + 0]]; unsigned b = reverse_lookup[(unsigned char)s[i + 1]]; unsigned c = reverse_lookup[(unsigned char)s[i + 2]]; unsigned d = reverse_lookup[(unsigned char)s[i + 3]]; unsigned u = a | b | c | d; if (unlikely(u & I)) goto invalid; if (unlikely(u & P)) { if (unlikely(i + 4 != len || (a | b) & P || (c & P && !(d & P)))) goto invalid; if (unlikely(size != NULL)) { if (c & P) *size = len / 4 * 3 - 2; else *size = len / 4 * 3 - 1; } c &= 63; d &= 63; } uint32_t v = a << 18 | b << 12 | c << 6 | d << 0; char x = (v >> 16) & 0xff; char y = (v >> 8) & 0xff; char z = (v >> 0) & 0xff; LOG_DBG("%c%c%c", x, y, z); ret[o + 0] = x; ret[o + 1] = y; ret[o + 2] = z; } ret[len / 4 * 3] = '\0'; return ret; invalid: free(ret); errno = EINVAL; return NULL; } char * base64_encode(const uint8_t *data, size_t size) { xassert(size % 3 == 0); if (unlikely(size % 3 != 0)) return NULL; char *ret = malloc(size / 3 * 4 + 1); if (unlikely(ret == NULL)) return NULL; for (size_t i = 0, o = 0; i < size; i += 3, o += 4) { int x = data[i + 0]; int y = data[i + 1]; int z = data[i + 2]; uint32_t v = x << 16 | y << 8 | z << 0; unsigned a = (v >> 18) & 0x3f; unsigned b = (v >> 12) & 0x3f; unsigned c = (v >> 6) & 0x3f; unsigned d = (v >> 0) & 0x3f; char c0 = lookup[a]; char c1 = lookup[b]; char c2 = lookup[c]; char c3 = lookup[d]; ret[o + 0] = c0; ret[o + 1] = c1; ret[o + 2] = c2; ret[o + 3] = c3; LOG_DBG("base64: encode: %c%c%c%c", c0, c1, c2, c3); } ret[size / 3 * 4] = '\0'; return ret; } void base64_encode_final(const uint8_t *data, size_t size, char result[4]) { xassert(size > 0); xassert(size < 3); uint32_t v = 0; if (size >= 1) v |= data[0] << 16; if (size >= 2) v |= data[1] << 8; unsigned a = (v >> 18) & 0x3f; unsigned b = (v >> 12) & 0x3f; unsigned c = (v >> 6) & 0x3f; char c0 = lookup[a]; char c1 = lookup[b]; char c2 = size == 2 ? lookup[c] : '='; char c3 = '='; result[0] = c0; result[1] = c1; result[2] = c2; result[3] = c3; LOG_DBG("base64: encode: %c%c%c%c", c0, c1, c2, c3); } foot-1.21.0/base64.h000066400000000000000000000003571476600145200140110ustar00rootroot00000000000000#pragma once #include #include char *base64_decode(const char *s, size_t *out_len); char *base64_encode(const uint8_t *data, size_t size); void base64_encode_final(const uint8_t *data, size_t size, char result[4]); foot-1.21.0/box-drawing.c000066400000000000000000003204201476600145200151350ustar00rootroot00000000000000#include "box-drawing.h" #include #include #include #include #define LOG_MODULE "box-drawing" #define LOG_ENABLE_DBG 0 #include "log.h" #include "config.h" #include "macros.h" #include "stride.h" #include "terminal.h" #include "util.h" #include "xmalloc.h" #define clamp(x, lower, upper) (min(upper, max(x, lower))) enum thickness { LIGHT, HEAVY, }; struct buf { uint8_t *data; pixman_image_t *pix; pixman_format_code_t format; int width; int height; int stride; bool solid_shades; int thickness[2]; /* For octants, sextants and wedges */ int x_halfs[2]; int y_thirds[2]; /* For octants */ int y_quads[3]; }; static const pixman_color_t white = {0xffff, 0xffff, 0xffff, 0xffff}; static void change_buffer_format(struct buf *buf, pixman_format_code_t new_format) { int stride = stride_for_format_and_width(new_format, buf->width); uint8_t *new_data = xcalloc(buf->height * stride, 1); pixman_image_t *new_pix = pixman_image_create_bits_no_clear( new_format, buf->width, buf->height, (uint32_t *)new_data, stride); if (new_pix == NULL) { errno = ENOMEM; perror(__func__); abort(); } pixman_image_unref(buf->pix); free(buf->data); buf->data = new_data; buf->pix = new_pix; buf->format = new_format; buf->stride = stride; } static int NOINLINE _thickness(int base_thickness, enum thickness thick) { int multiplier = thick * 2 + 1; xassert(base_thickness >= 1); xassert((thick == LIGHT && multiplier == 1) || (thick == HEAVY && multiplier == 3)); return base_thickness * multiplier; } #define thickness(thick) buf->thickness[thick] static void NOINLINE _hline(struct buf *buf, int x1, int x2, int y, int thick) { pixman_box32_t box = { .x1 = min(max(x1, 0), buf->width), .x2 = min(max(x2, 0), buf->width), .y1 = min(max(y, 0), buf->height), .y2 = min(max(y + thick, 0), buf->height), }; pixman_image_fill_boxes(PIXMAN_OP_SRC, buf->pix, &white, 1, &box); } #define hline(x1, x2, y, thick) _hline(buf, x1, x2, y, thick) static void NOINLINE _vline(struct buf *buf, int y1, int y2, int x, int thick) { pixman_box32_t box = { .x1 = min(max(x, 0), buf->width), .x2 = min(max(x + thick, 0), buf->width), .y1 = min(max(y1, 0), buf->height), .y2 = min(max(y2, 0), buf->height), }; pixman_image_fill_boxes(PIXMAN_OP_SRC, buf->pix, &white, 1, &box); } #define vline(y1, y2, x, thick) _vline(buf, y1, y2, x, thick) static void NOINLINE _rect(struct buf *buf, int x1, int y1, int x2, int y2) { pixman_box32_t box = { .x1 = min(max(x1, 0), buf->width), .y1 = min(max(y1, 0), buf->height), .x2 = min(max(x2, 0), buf->width), .y2 = min(max(y2, 0), buf->height), }; pixman_image_fill_boxes(PIXMAN_OP_SRC, buf->pix, &white, 1, &box); } #define rect(x1, y1, x2, y2) _rect(buf, x1, y1, x2, y2) static void NOINLINE _hline_middle(struct buf *buf, enum thickness _thick) { int thick = thickness(_thick); hline(0, buf->width, (buf->height - thick) / 2, thick); } static void NOINLINE _hline_middle_left(struct buf *buf, enum thickness _vthick, enum thickness _hthick) { int vthick = thickness(_vthick); int hthick = thickness(_hthick); _hline(buf, 0, (buf->width + vthick) / 2, (buf->height - hthick) / 2, hthick); } static void NOINLINE _hline_middle_right(struct buf *buf, enum thickness _vthick, enum thickness _hthick) { int vthick = thickness(_vthick); int hthick = thickness(_hthick); hline((buf->width - vthick) / 2, buf->width, (buf->height - hthick) / 2, hthick); } static void NOINLINE _vline_middle(struct buf *buf, enum thickness _thick) { int thick = thickness(_thick); vline(0, buf->height, (buf->width - thick) / 2, thick); } static void NOINLINE _vline_middle_up(struct buf *buf, enum thickness _vthick, enum thickness _hthick) { int vthick = thickness(_vthick); int hthick = thickness(_hthick); vline(0, (buf->height + hthick) / 2, (buf->width - vthick) / 2, vthick); } static void NOINLINE _vline_middle_down(struct buf *buf, enum thickness _vthick, enum thickness _hthick) { int vthick = thickness(_vthick); int hthick = thickness(_hthick); vline((buf->height - hthick) / 2, buf->height, (buf->width - vthick) / 2, vthick); } #define hline_middle(thick) _hline_middle(buf, thick) #define hline_middle_left(thick) _hline_middle_left(buf, thick, thick) #define hline_middle_right(thick) _hline_middle_right(buf, thick, thick) #define hline_middle_left_mixed(vthick, hthick) _hline_middle_left(buf, vthick, hthick) #define hline_middle_right_mixed(vthick, hthick) _hline_middle_right(buf, vthick, hthick) #define vline_middle(thick) _vline_middle(buf, thick) #define vline_middle_up(thick) _vline_middle_up(buf, thick, thick) #define vline_middle_down(thick) _vline_middle_down(buf, thick, thick) #define vline_middle_up_mixed(vthick, hthick) _vline_middle_up(buf, vthick, hthick) #define vline_middle_down_mixed(vthick, hthick) _vline_middle_down(buf, vthick, hthick) static void draw_box_drawings_light_horizontal(struct buf *buf) { hline_middle(LIGHT); } static void draw_box_drawings_heavy_horizontal(struct buf *buf) { hline_middle(HEAVY); } static void draw_box_drawings_light_vertical(struct buf *buf) { vline_middle(LIGHT); } static void draw_box_drawings_heavy_vertical(struct buf *buf) { vline_middle(HEAVY); } static void draw_box_drawings_dash_horizontal(struct buf *buf, int count, int thick, int gap) { int width = buf->width; int height = buf->height; xassert(count >= 2 && count <= 4); const int gap_count = count - 1; int dash_width = (width - (gap_count * gap)) / count; while (dash_width <= 0 && gap > 1) { gap--; dash_width = (width - (gap_count * gap)) / count; } if (dash_width <= 0) { hline_middle(LIGHT); return; } xassert(count * dash_width + gap_count * gap <= width); int remaining = width - count * dash_width - gap_count * gap; int x[4] = {0}; int w[4] = {dash_width, dash_width, dash_width, dash_width}; x[0] = 0; x[1] = x[0] + w[0] + gap; if (count == 2) w[1] = width - x[1]; else if (count == 3) w[1] += remaining; else w[1] += remaining / 2; if (count >= 3) { x[2] = x[1] + w[1] + gap; if (count == 3) w[2] = width - x[2]; else w[2] += remaining - remaining / 2; } if (count >= 4) { x[3] = x[2] + w[2] + gap; w[3] = width - x[3]; } hline(x[0], x[0] + w[0], (height - thick) / 2, thick); hline(x[1], x[1] + w[1], (height - thick) / 2, thick); if (count >= 3) hline(x[2], x[2] + w[2], (height - thick) / 2, thick); if (count >= 4) hline(x[3], x[3] + w[3], (height - thick) / 2, thick); } static void draw_box_drawings_dash_vertical(struct buf *buf, int count, int thick, int gap) { int width = buf->width; int height = buf->height; xassert(count >= 2 && count <= 4); const int gap_count = count - 1; int dash_height = (height - (gap_count * gap)) / count; while (dash_height <= 0 && gap > 1) { gap--; dash_height = (height - (gap_count * gap)) / count; } if (dash_height <= 0) { vline_middle(LIGHT); return; } xassert(count * dash_height + gap_count * gap <= height); int remaining = height - count * dash_height - gap_count * gap; int y[4] = {0}; int h[4] = {dash_height, dash_height, dash_height, dash_height}; y[0] = 0; y[1] = y[0] + h[0] + gap; if (count == 2) h[1] = height - y[1]; else if (count == 3) h[1] += remaining; else h[1] += remaining / 2; if (count >= 3) { y[2] = y[1] + h[1] + gap; if (count == 3) h[2] = height - y[2]; else h[2] += remaining - remaining / 2; } if (count >= 4) { y[3] = y[2] + h[2] + gap; h[3] = height - y[3]; } vline(y[0], y[0] + h[0], (width - thick) / 2, thick); vline(y[1], y[1] + h[1], (width - thick) / 2, thick); if (count >= 3) vline(y[2], y[2] + h[2], (width - thick) / 2, thick); if (count >= 4) vline(y[3], y[3] + h[3], (width - thick) / 2, thick); } static void draw_box_drawings_light_triple_dash_horizontal(struct buf *buf) { draw_box_drawings_dash_horizontal(buf, 3, thickness(LIGHT), thickness(LIGHT)); } static void draw_box_drawings_heavy_triple_dash_horizontal(struct buf *buf) { draw_box_drawings_dash_horizontal(buf, 3, thickness(HEAVY), thickness(LIGHT)); } static void draw_box_drawings_light_triple_dash_vertical(struct buf *buf) { draw_box_drawings_dash_vertical(buf, 3, thickness(LIGHT), thickness(HEAVY)); } static void draw_box_drawings_heavy_triple_dash_vertical(struct buf *buf) { draw_box_drawings_dash_vertical(buf, 3, thickness(HEAVY), thickness(HEAVY)); } static void draw_box_drawings_light_quadruple_dash_horizontal(struct buf *buf) { draw_box_drawings_dash_horizontal(buf, 4, thickness(LIGHT), thickness(LIGHT)); } static void draw_box_drawings_heavy_quadruple_dash_horizontal(struct buf *buf) { draw_box_drawings_dash_horizontal(buf, 4, thickness(HEAVY), thickness(LIGHT)); } static void draw_box_drawings_light_quadruple_dash_vertical(struct buf *buf) { draw_box_drawings_dash_vertical(buf, 4, thickness(LIGHT), thickness(LIGHT)); } static void draw_box_drawings_heavy_quadruple_dash_vertical(struct buf *buf) { draw_box_drawings_dash_vertical(buf, 4, thickness(HEAVY), thickness(LIGHT)); } static void draw_box_drawings_light_down_and_right(struct buf *buf) { hline_middle_right(LIGHT); vline_middle_down(LIGHT); } static void draw_box_drawings_down_light_and_right_heavy(struct buf *buf) { hline_middle_right_mixed(LIGHT, HEAVY); vline_middle_down(LIGHT); } static void draw_box_drawings_down_heavy_and_right_light(struct buf *buf) { hline_middle_right(LIGHT); vline_middle_down_mixed(HEAVY, LIGHT); } static void draw_box_drawings_heavy_down_and_right(struct buf *buf) { hline_middle_right(HEAVY); vline_middle_down(HEAVY); } static void draw_box_drawings_light_down_and_left(struct buf *buf) { hline_middle_left(LIGHT); vline_middle_down(LIGHT); } static void draw_box_drawings_down_light_and_left_heavy(struct buf *buf) { hline_middle_left_mixed(LIGHT, HEAVY); vline_middle_down(LIGHT); } static void draw_box_drawings_down_heavy_and_left_light(struct buf *buf) { hline_middle_left(LIGHT); vline_middle_down_mixed(HEAVY, LIGHT); } static void draw_box_drawings_heavy_down_and_left(struct buf *buf) { hline_middle_left(HEAVY); vline_middle_down(HEAVY); } static void draw_box_drawings_light_up_and_right(struct buf *buf) { hline_middle_right(LIGHT); vline_middle_up(LIGHT); } static void draw_box_drawings_up_light_and_right_heavy(struct buf *buf) { hline_middle_right_mixed(LIGHT, HEAVY); vline_middle_up(LIGHT); } static void draw_box_drawings_up_heavy_and_right_light(struct buf *buf) { hline_middle_right(LIGHT); vline_middle_up_mixed(HEAVY, LIGHT); } static void draw_box_drawings_heavy_up_and_right(struct buf *buf) { hline_middle_right(HEAVY); vline_middle_up(HEAVY); } static void draw_box_drawings_light_up_and_left(struct buf *buf) { hline_middle_left(LIGHT); vline_middle_up(LIGHT); } static void draw_box_drawings_up_light_and_left_heavy(struct buf *buf) { hline_middle_left_mixed(LIGHT, HEAVY); vline_middle_up(LIGHT); } static void draw_box_drawings_up_heavy_and_left_light(struct buf *buf) { hline_middle_left(LIGHT); vline_middle_up_mixed(HEAVY, LIGHT); } static void draw_box_drawings_heavy_up_and_left(struct buf *buf) { hline_middle_left(HEAVY); vline_middle_up(HEAVY); } static void draw_box_drawings_light_vertical_and_right(struct buf *buf) { hline_middle_right(LIGHT); vline_middle(LIGHT); } static void draw_box_drawings_vertical_light_and_right_heavy(struct buf *buf) { hline_middle_right_mixed(LIGHT, HEAVY); vline_middle(LIGHT); } static void draw_box_drawings_up_heavy_and_right_down_light(struct buf *buf) { hline_middle_right(LIGHT); vline_middle_up_mixed(HEAVY, LIGHT); vline_middle_down(LIGHT); } static void draw_box_drawings_down_heavy_and_right_up_light(struct buf *buf) { hline_middle_right(LIGHT); vline_middle_up(LIGHT); vline_middle_down_mixed(HEAVY, LIGHT); } static void draw_box_drawings_vertical_heavy_and_right_light(struct buf *buf) { hline_middle_right(LIGHT); vline_middle(HEAVY); } static void draw_box_drawings_down_light_and_right_up_heavy(struct buf *buf) { hline_middle_right(HEAVY); vline_middle_up(HEAVY); vline_middle_down(LIGHT); } static void draw_box_drawings_up_light_and_right_down_heavy(struct buf *buf) { hline_middle_right(HEAVY); vline_middle_up(LIGHT); vline_middle_down(HEAVY); } static void draw_box_drawings_heavy_vertical_and_right(struct buf *buf) { hline_middle_right(HEAVY); vline_middle(HEAVY); } static void draw_box_drawings_light_vertical_and_left(struct buf *buf) { hline_middle_left(LIGHT); vline_middle(LIGHT); } static void draw_box_drawings_vertical_light_and_left_heavy(struct buf *buf) { hline_middle_left_mixed(LIGHT, HEAVY); vline_middle(LIGHT); } static void draw_box_drawings_up_heavy_and_left_down_light(struct buf *buf) { hline_middle_left(LIGHT); vline_middle_up_mixed(HEAVY, LIGHT); vline_middle_down(LIGHT); } static void draw_box_drawings_down_heavy_and_left_up_light(struct buf *buf) { hline_middle_left(LIGHT); vline_middle_up(LIGHT); vline_middle_down_mixed(HEAVY, LIGHT); } static void draw_box_drawings_vertical_heavy_and_left_light(struct buf *buf) { hline_middle_left(LIGHT); vline_middle(HEAVY); } static void draw_box_drawings_down_light_and_left_up_heavy(struct buf *buf) { hline_middle_left(HEAVY); vline_middle_up(HEAVY); vline_middle_down(LIGHT); } static void draw_box_drawings_up_light_and_left_down_heavy(struct buf *buf) { hline_middle_left(HEAVY); vline_middle_up(LIGHT); vline_middle_down(HEAVY); } static void draw_box_drawings_heavy_vertical_and_left(struct buf *buf) { hline_middle_left(HEAVY); vline_middle(HEAVY); } static void draw_box_drawings_light_down_and_horizontal(struct buf *buf) { hline_middle(LIGHT); vline_middle_down(LIGHT); } static void draw_box_drawings_left_heavy_and_right_down_light(struct buf *buf) { hline_middle_left_mixed(LIGHT, HEAVY); hline_middle_right(LIGHT); vline_middle_down(LIGHT); } static void draw_box_drawings_right_heavy_and_left_down_light(struct buf *buf) { hline_middle_left(LIGHT); hline_middle_right_mixed(LIGHT, HEAVY); vline_middle_down(LIGHT); } static void draw_box_drawings_down_light_and_horizontal_heavy(struct buf *buf) { hline_middle(HEAVY); vline_middle_down(LIGHT); } static void draw_box_drawings_down_heavy_and_horizontal_light(struct buf *buf) { hline_middle(LIGHT); vline_middle_down_mixed(HEAVY, LIGHT); } static void draw_box_drawings_right_light_and_left_down_heavy(struct buf *buf) { hline_middle_left(HEAVY); hline_middle_right(LIGHT); vline_middle_down(HEAVY); } static void draw_box_drawings_left_light_and_right_down_heavy(struct buf *buf) { hline_middle_left(LIGHT); hline_middle_right(HEAVY); vline_middle_down(HEAVY); } static void draw_box_drawings_heavy_down_and_horizontal(struct buf *buf) { hline_middle(HEAVY); vline_middle_down(HEAVY); } static void draw_box_drawings_light_up_and_horizontal(struct buf *buf) { hline_middle(LIGHT); vline_middle_up(LIGHT); } static void draw_box_drawings_left_heavy_and_right_up_light(struct buf *buf) { hline_middle_left_mixed(LIGHT, HEAVY); hline_middle_right(LIGHT); vline_middle_up(LIGHT); } static void draw_box_drawings_right_heavy_and_left_up_light(struct buf *buf) { hline_middle_left(LIGHT); hline_middle_right_mixed(LIGHT, HEAVY); vline_middle_up(LIGHT); } static void draw_box_drawings_up_light_and_horizontal_heavy(struct buf *buf) { hline_middle(HEAVY); vline_middle_up(LIGHT); } static void draw_box_drawings_up_heavy_and_horizontal_light(struct buf *buf) { hline_middle(LIGHT); vline_middle_up_mixed(HEAVY, LIGHT); } static void draw_box_drawings_right_light_and_left_up_heavy(struct buf *buf) { hline_middle_left(HEAVY); hline_middle_right(LIGHT); vline_middle_up(HEAVY); } static void draw_box_drawings_left_light_and_right_up_heavy(struct buf *buf) { hline_middle_left(LIGHT); hline_middle_right(HEAVY); vline_middle_up(HEAVY); } static void draw_box_drawings_heavy_up_and_horizontal(struct buf *buf) { hline_middle(HEAVY); vline_middle_up(HEAVY); } static void draw_box_drawings_light_vertical_and_horizontal(struct buf *buf) { hline_middle(LIGHT); vline_middle(LIGHT); } static void draw_box_drawings_left_heavy_and_right_vertical_light(struct buf *buf) { hline_middle_left_mixed(LIGHT, HEAVY); hline_middle_right(LIGHT); vline_middle(LIGHT); } static void draw_box_drawings_right_heavy_and_left_vertical_light(struct buf *buf) { hline_middle_left(LIGHT); hline_middle_right_mixed(LIGHT, HEAVY); vline_middle(LIGHT); } static void draw_box_drawings_vertical_light_and_horizontal_heavy(struct buf *buf) { hline_middle(HEAVY); vline_middle(LIGHT); } static void draw_box_drawings_up_heavy_and_down_horizontal_light(struct buf *buf) { hline_middle(LIGHT); vline_middle_up_mixed(HEAVY, LIGHT); vline_middle_down(LIGHT); } static void draw_box_drawings_down_heavy_and_up_horizontal_light(struct buf *buf) { hline_middle(LIGHT); vline_middle_up(LIGHT); vline_middle_down_mixed(HEAVY, LIGHT); } static void draw_box_drawings_vertical_heavy_and_horizontal_light(struct buf *buf) { hline_middle(LIGHT); vline_middle(HEAVY); } static void draw_box_drawings_left_up_heavy_and_right_down_light(struct buf *buf) { hline_middle_left(HEAVY); hline_middle_right(LIGHT); vline_middle_up(HEAVY); vline_middle_down(LIGHT); } static void draw_box_drawings_right_up_heavy_and_left_down_light(struct buf *buf) { hline_middle_left(LIGHT); hline_middle_right(HEAVY); vline_middle_up(HEAVY); vline_middle_down(LIGHT); } static void draw_box_drawings_left_down_heavy_and_right_up_light(struct buf *buf) { hline_middle_left(HEAVY); hline_middle_right(LIGHT); vline_middle_up(LIGHT); vline_middle_down(HEAVY); } static void draw_box_drawings_right_down_heavy_and_left_up_light(struct buf *buf) { hline_middle_left(LIGHT); hline_middle_right(HEAVY); vline_middle_up(LIGHT); vline_middle_down(HEAVY); } static void draw_box_drawings_down_light_and_up_horizontal_heavy(struct buf *buf) { hline_middle(HEAVY); vline_middle_up(HEAVY); vline_middle_down(LIGHT); } static void draw_box_drawings_up_light_and_down_horizontal_heavy(struct buf *buf) { hline_middle(HEAVY); vline_middle_up(LIGHT); vline_middle_down(HEAVY); } static void draw_box_drawings_right_light_and_left_vertical_heavy(struct buf *buf) { hline_middle_left(HEAVY); hline_middle_right(LIGHT); vline_middle(HEAVY); } static void draw_box_drawings_left_light_and_right_vertical_heavy(struct buf *buf) { hline_middle_left(LIGHT); hline_middle_right(HEAVY); vline_middle(HEAVY); } static void draw_box_drawings_heavy_vertical_and_horizontal(struct buf *buf) { hline_middle(HEAVY); vline_middle(HEAVY); } static void draw_box_drawings_light_double_dash_horizontal(struct buf *buf) { draw_box_drawings_dash_horizontal(buf, 2, thickness(LIGHT), thickness(LIGHT)); } static void draw_box_drawings_heavy_double_dash_horizontal(struct buf *buf) { draw_box_drawings_dash_horizontal(buf, 2, thickness(HEAVY), thickness(LIGHT)); } static void draw_box_drawings_light_double_dash_vertical(struct buf *buf) { draw_box_drawings_dash_vertical(buf, 2, thickness(LIGHT), thickness(HEAVY)); } static void draw_box_drawings_heavy_double_dash_vertical(struct buf *buf) { draw_box_drawings_dash_vertical(buf, 2, thickness(HEAVY), thickness(HEAVY)); } static void draw_box_drawings_double_horizontal(struct buf *buf) { int thick = thickness(LIGHT); int mid = (buf->height - thick * 3) / 2; hline(0, buf->width, mid, thick); hline(0, buf->width, mid + 2 * thick, thick); } static void draw_box_drawings_double_vertical(struct buf *buf) { int thick = thickness(LIGHT); int mid = (buf->width - thick * 3) / 2; vline(0, buf->height, mid, thick); vline(0, buf->height, mid + 2 * thick, thick); } static void draw_box_drawings_down_single_and_right_double(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width - thick) / 2; vline_middle_down(LIGHT); hline(vmid, buf->width, hmid, thick); hline(vmid, buf->width, hmid + 2 * thick, thick); } static void draw_box_drawings_down_double_and_right_single(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick) / 2; int vmid = (buf->width - thick * 3) / 2; hline_middle_right(LIGHT); vline(hmid, buf->height, vmid, thick); vline(hmid, buf->height, vmid + 2 * thick, thick); } static void draw_box_drawings_double_down_and_right(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width - thick * 3) / 2; vline(hmid, buf->height, vmid, thick); vline(hmid + 2 * thick, buf->height, vmid + 2 * thick, thick); hline(vmid, buf->width, hmid, thick); hline(vmid + 2 * thick, buf->width, hmid + 2 * thick, thick); } static void draw_box_drawings_down_single_and_left_double(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width + thick) / 2; vline_middle_down(LIGHT); hline(0, vmid, hmid, thick); hline(0, vmid, hmid + 2 * thick, thick); } static void draw_box_drawings_down_double_and_left_single(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick) / 2; int vmid = (buf->width - thick * 3) / 2; hline_middle_left(LIGHT); vline(hmid, buf->height, vmid, thick); vline(hmid, buf->height, vmid + 2 * thick, thick); } static void draw_box_drawings_double_down_and_left(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width - thick * 3) / 2; vline(hmid + 2 * thick, buf->height, vmid, thick); vline(hmid, buf->height, vmid + 2 * thick, thick); hline(0, vmid + 2 * thick, hmid, thick); hline(0, vmid, hmid + 2 * thick, thick); } static void draw_box_drawings_up_single_and_right_double(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width - thick) / 2; vline_middle_up(LIGHT); hline(vmid, buf->width, hmid, thick); hline(vmid, buf->width, hmid + 2 * thick, thick); } static void draw_box_drawings_up_double_and_right_single(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height + thick) / 2; int vmid = (buf->width - thick * 3) / 2; hline_middle_right(LIGHT); vline(0, hmid, vmid, thick); vline(0, hmid, vmid + 2 * thick, thick); } static void draw_box_drawings_double_up_and_right(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width - thick * 3) / 2; vline(0, hmid + 2 * thick, vmid, thick); vline(0, hmid, vmid + 2 * thick, thick); hline(vmid + 2 * thick, buf->width, hmid, thick); hline(vmid, buf->width, hmid + 2 * thick, thick); } static void draw_box_drawings_up_single_and_left_double(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width + thick) / 2; vline_middle_up(LIGHT); hline(0, vmid, hmid, thick); hline(0, vmid, hmid + 2 * thick, thick); } static void draw_box_drawings_up_double_and_left_single(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height + thick) / 2; int vmid = (buf->width - thick * 3) / 2; hline_middle_left(LIGHT); vline(0, hmid, vmid, thick); vline(0, hmid, vmid + 2 * thick, thick); } static void draw_box_drawings_double_up_and_left(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width - thick * 3) / 2; vline(0, hmid + 0 * thick + thick, vmid, thick); vline(0, hmid + 2 * thick + thick, vmid + 2 * thick, thick); hline(0, vmid, hmid, thick); hline(0, vmid + 2 * thick, hmid + 2 * thick, thick); } static void draw_box_drawings_vertical_single_and_right_double(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width - thick) / 2; vline_middle(LIGHT); hline(vmid, buf->width, hmid, thick); hline(vmid, buf->width, hmid + 2 * thick, thick); } static void draw_box_drawings_vertical_double_and_right_single(struct buf *buf) { int thick = thickness(LIGHT); int vmid = (buf->width - thick * 3) / 2; hline(vmid + 2 * thick, buf->width, (buf->height - thick) / 2, thick); vline(0, buf->height, vmid, thick); vline(0, buf->height, vmid + 2 * thick, thick); } static void draw_box_drawings_double_vertical_and_right(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width - thick * 3) / 2; vline(0, buf->height, vmid, thick); vline(0, hmid, vmid + 2 * thick, thick); vline(hmid + 2 * thick, buf->height, vmid + 2 * thick, thick); hline(vmid + 2 * thick, buf->width, hmid, thick); hline(vmid + 2 * thick, buf->width, hmid + 2 * thick, thick); } static void draw_box_drawings_vertical_single_and_left_double(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width + thick) / 2; vline_middle(LIGHT); hline(0, vmid, hmid, thick); hline(0, vmid, hmid + 2 * thick, thick); } static void draw_box_drawings_vertical_double_and_left_single(struct buf *buf) { int thick = thickness(LIGHT); int vmid = (buf->width - thick * 3) / 2; hline(0, vmid, (buf->height - thick) / 2, thick); vline(0, buf->height, vmid, thick); vline(0, buf->height, vmid + 2 * thick, thick); } static void draw_box_drawings_double_vertical_and_left(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width - thick * 3) / 2; vline(0, buf->height, vmid + 2 * thick, thick); vline(0, hmid, vmid, thick); vline(hmid + 2 * thick, buf->height, vmid, thick); hline(0, vmid + thick, hmid, thick); hline(0, vmid, hmid + 2 * thick, thick); } static void draw_box_drawings_down_single_and_horizontal_double(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; vline(hmid + 2 * thick, buf->height, (buf->width - thick) / 2, thick); hline(0, buf->width, hmid, thick); hline(0, buf->width, hmid + 2 * thick, thick); } static void draw_box_drawings_down_double_and_horizontal_single(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick) / 2; int vmid = (buf->width - thick * 3) / 2; hline_middle(LIGHT); vline(hmid, buf->height, vmid, thick); vline(hmid, buf->height, vmid + 2 * thick, thick); } static void draw_box_drawings_double_down_and_horizontal(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width - thick * 3) / 2; hline(0, buf->width, hmid, thick); hline(0, vmid, hmid + 2 * thick, thick); hline(vmid + 2 * thick, buf->width, hmid + 2 * thick, thick); vline(hmid + 2 * thick, buf->height, vmid, thick); vline(hmid + 2 * thick, buf->height, vmid + 2 * thick, thick); } static void draw_box_drawings_up_single_and_horizontal_double(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width - thick) / 2; vline(0, hmid, vmid, thick); hline(0, buf->width, hmid, thick); hline(0, buf->width, hmid + 2 * thick, thick); } static void draw_box_drawings_up_double_and_horizontal_single(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick) / 2; int vmid = (buf->width - thick * 3) / 2; hline_middle(LIGHT); vline(0, hmid, vmid, thick); vline(0, hmid, vmid + 2 * thick, thick); } static void draw_box_drawings_double_up_and_horizontal(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width - thick * 3) / 2; vline(0, hmid, vmid, thick); vline(0, hmid, vmid + 2 * thick, thick); hline(0, vmid + thick, hmid, thick); hline(vmid + 2 * thick, buf->width, hmid, thick); hline(0, buf->width, hmid + 2 * thick, thick); } static void draw_box_drawings_vertical_single_and_horizontal_double(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; vline_middle(LIGHT); hline(0, buf->width, hmid, thick); hline(0, buf->width, hmid + 2 * thick, thick); } static void draw_box_drawings_vertical_double_and_horizontal_single(struct buf *buf) { int thick = thickness(LIGHT); int vmid = (buf->width - thick * 3) / 2; hline_middle(LIGHT); vline(0, buf->height, vmid, thick); vline(0, buf->height, vmid + 2 * thick, thick); } static void draw_box_drawings_double_vertical_and_horizontal(struct buf *buf) { int thick = thickness(LIGHT); int hmid = (buf->height - thick * 3) / 2; int vmid = (buf->width - thick * 3) / 2; hline(0, vmid, hmid, thick); hline(vmid + 2 * thick, buf->width, hmid, thick); hline(0, vmid, hmid + 2 * thick, thick); hline(vmid + 2 * thick, buf->width, hmid + 2 * thick, thick); vline(0, hmid + thick, vmid, thick); vline(0, hmid, vmid + 2 * thick, thick); vline(hmid + 2 * thick, buf->height, vmid, thick); vline(hmid + 2 * thick, buf->height, vmid + 2 * thick, thick); } static inline void set_a1_bit(uint8_t *data, size_t ofs, size_t bit_no) { #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ data[ofs] |= 1 << bit_no; #else data[ofs] |= 1 << (7 - bit_no); #endif } static void NOINLINE draw_box_drawings_light_arc(struct buf *buf, char32_t wc) { const pixman_format_code_t fmt = buf->format; const int supersample = fmt == PIXMAN_a8 ? 4 : 1; const int height = buf->height * supersample; const int width = buf->width * supersample; const int stride = fmt == PIXMAN_a8 ? stride_for_format_and_width(PIXMAN_a8, width) : buf->stride; uint8_t *data = supersample > 1 ? xcalloc(height * stride, 1) : buf->data; const int thick = thickness(LIGHT) * supersample; const int height_pixels = buf->height; const int width_pixels = buf->width; const int thick_pixels = thickness(LIGHT); /* * The general idea here is to connect the two incoming lines using a * circle, which is extended to the box-edges with vertical/horizontal * lines. * * The radius of the quartercircle should be as big as possible, with some * restrictions: The radius should be the same for all of ╭ ╮ ╯ ╰ at a * given box-size (which won't be the case if we choose the biggest * possible radius for a given box, consider the following:) * * * ▕ ███ ▏ * ▕a │ d▔▔ * ▕ │ x * ▕ │ * ▕ │ ██ * ▕ ╰────██ * ▕ ██ * ▕ * ▕ * ▕b c * ▔▔▔▔▔▔▔▔▔▔ * for ╰ it would be possible to center the circle on the upper right * corner of d, but we have set it on x instead because ╯ can only use a * 2px inner radius: * * ▕ ███ ▏ * ▔▔a │ d▏ * x │ ▏ * │ ▏ * ██ │ ▏ * ██───╯ ▏ * ██ ▏ * ▏ * ▏ * b c▏ * ▔▔▔▔▔▔▔▔▔▔ * As the incoming lines always exactly fill pixels, and are rounded down * (via float->int), we can use this to get the radius of the inner * (connecting the left/upper edge of the lines) quartercircle. */ int circle_inner_edge = (min(width_pixels, height_pixels) - thick_pixels) / 2; /* * We want to draw the quartercircle by filling small circles (with r = * thickness/2.) whose centers are on its edge. This means to get the * radius of the quartercircle, we add the exact half thickness to the * radius of the inner circle. */ double c_r = circle_inner_edge + thick_pixels/2.; /* * We need to draw short lines from the end of the quartercircle to the * box-edges, store one endpoint (the other is the edge of the * quartercircle) in these vars. */ int vert_to = 0, hor_to = 0; /* Coordinates of the circle-center. */ int c_x = 0, c_y = 0; /* * For a given y there are up to two solutions for the circle-equation. * Set to -1 for the left, and 1 for the right hemisphere. */ int circle_hemisphere = 0; /* * The quarter circle only has to be evaluated for a small range of * y-values. */ int y_min = 0, y_max = 0; switch (wc) { case U'╭': { /* * Don't use supersampled coordinates yet, we want to align actual * pixels. * * pixel-coordinates of the lower edge of the right line and the * right edge of the bottom line. */ int right_bottom_edge = (height_pixels + thick_pixels) / 2; int bottom_right_edge = (width_pixels + thick_pixels) / 2; /* find coordinates of circle-center. */ c_y = right_bottom_edge + circle_inner_edge; c_x = bottom_right_edge + circle_inner_edge; /* we want to render the left, not the right hemisphere of the circle. */ circle_hemisphere = -1; /* don't evaluate beyond c_y, the vertical line is drawn there. */ y_min = 0; y_max = c_y; /* * the vertical line should extend to the bottom of the box, the * horizontal to the right. */ vert_to = height_pixels; hor_to = width_pixels; break; } case U'╮': { int left_bottom_edge = (height_pixels + thick_pixels) / 2; int bottom_left_edge = (width_pixels - thick_pixels) / 2; c_y = left_bottom_edge + circle_inner_edge; c_x = bottom_left_edge - circle_inner_edge; circle_hemisphere = 1; y_min = 0; y_max = c_y; vert_to = height_pixels; hor_to = 0; break; } case U'╰': { int right_top_edge = (height_pixels - thick_pixels) / 2; int top_right_edge = (width_pixels + thick_pixels) / 2; c_y = right_top_edge - circle_inner_edge; c_x = top_right_edge + circle_inner_edge; circle_hemisphere = -1; y_min = c_y; y_max = height_pixels; vert_to = 0; hor_to = width_pixels; break; } case U'╯': { int left_top_edge = (height_pixels - thick_pixels) / 2; int top_left_edge = (width_pixels - thick_pixels) / 2; c_y = left_top_edge - circle_inner_edge; c_x = top_left_edge - circle_inner_edge; circle_hemisphere = 1; y_min = c_y; y_max = height_pixels; vert_to = 0; hor_to = 0; break; } } /* store for horizontal+vertical line. */ int c_x_pixels = c_x; int c_y_pixels = c_y; /* Bring coordinates from pixel-grid to supersampled grid. */ c_r *= supersample; c_x *= supersample; c_y *= supersample; y_min *= supersample; y_max *= supersample; double c_r2 = c_r * c_r; /* * To prevent gaps in the circle, each pixel is sampled multiple times. * As the quartercircle ends (vertically) in the middle of a pixel, an * uneven number helps hit that exactly. */ for (double i = y_min*16; i <= y_max*16; i++) { errno = 0; double y = i / 16.; double x = circle_hemisphere * sqrt(c_r2 - (y - c_y) * (y - c_y)) + c_x; /* See math_error(7) */ if (errno != 0) { continue; } const int row = round(y); const int col = round(x); if (col < 0) continue; /* rectangle big enough to fit entire circle with radius thick/2. */ int row1 = row - (thick/2+1); int row2 = row + (thick/2+1); int col1 = col - (thick/2+1); int col2 = col + (thick/2+1); int row_start = min(row1, row2), row_end = max(row1, row2), col_start = min(col1, col2), col_end = max(col1, col2); xassert(row_end > row_start); xassert(col_end > col_start); /* * draw circle with radius thick/2 around x,y. * this is accomplished by rejecting pixels where the distance from * their center to x,y is greater than thick/2. */ for (int r = max(row_start, 0); r < max(min(row_end, height), 0); r++) { double r_midpoint = r + 0.5; for (int c = max(col_start, 0); c < max(min(col_end, width), 0); c++) { double c_midpoint = c + 0.5; /* vector from point on quartercircle to midpoint of the current pixel. */ double center_midpoint_x = c_midpoint - x; double center_midpoint_y = r_midpoint - y; /* distance from current point to circle-center. */ double dist = sqrt(center_midpoint_x * center_midpoint_x + center_midpoint_y * center_midpoint_y); /* skip if midpoint of pixel is outside the circle. */ if (dist > thick/2.) continue; if (fmt == PIXMAN_a1) { size_t idx = c / 8; size_t bit_no = c % 8; set_a1_bit(data, r * stride + idx, bit_no); } else data[r * stride + c] = 0xff; } } } if (fmt == PIXMAN_a8) { xassert(data != buf->data); /* Downsample */ for (size_t r = 0; r < buf->height; r++) { for (size_t c = 0; c < buf->width; c++) { uint32_t total = 0; for (size_t i = 0; i < supersample; i++) { for (size_t j = 0; j < supersample; j++) total += data[(r * supersample + i) * stride + c * supersample + j]; } uint8_t average = min(total / (supersample * supersample), 0xff); buf->data[r * buf->stride + c] = average; } } free(data); } /* draw vertical/horizontal lines from quartercircle-edge to box-edge. */ vline(min(c_y_pixels, vert_to), max(c_y_pixels, vert_to), (width_pixels - thick_pixels) / 2, thick_pixels); hline(min(c_x_pixels, hor_to), max(c_x_pixels, hor_to), (height_pixels - thick_pixels) / 2, thick_pixels); } static void NOINLINE draw_box_drawings_light_diagonal_upper_right_to_lower_left(struct buf *buf) { pixman_trapezoid_t trap = { .top = pixman_int_to_fixed(0), .bottom = pixman_int_to_fixed(buf->height), .left = { .p1 = { .x = pixman_double_to_fixed(buf->width - thickness(LIGHT) / 2.), .y = pixman_int_to_fixed(0), }, .p2 = { .x = pixman_double_to_fixed(0 - thickness(LIGHT) / 2.), .y = pixman_int_to_fixed(buf->height), }, }, .right = { .p1 = { .x = pixman_double_to_fixed(buf->width + thickness(LIGHT) / 2.), .y = pixman_int_to_fixed(0), }, .p2 = { .x = pixman_double_to_fixed(0 + thickness(LIGHT) / 2.), .y = pixman_int_to_fixed(buf->height), }, }, }; pixman_rasterize_trapezoid(buf->pix, &trap, 0, 0); } static void NOINLINE draw_box_drawings_light_diagonal_upper_left_to_lower_right(struct buf *buf) { pixman_trapezoid_t trap = { .top = pixman_int_to_fixed(0), .bottom = pixman_int_to_fixed(buf->height), .left = { .p1 = { .x = pixman_double_to_fixed(0 - thickness(LIGHT) / 2.), .y = pixman_int_to_fixed(0), }, .p2 = { .x = pixman_double_to_fixed(buf->width - thickness(LIGHT) / 2.), .y = pixman_int_to_fixed(buf->height), }, }, .right = { .p1 = { .x = pixman_double_to_fixed(0 + thickness(LIGHT) / 2.), .y = pixman_int_to_fixed(0), }, .p2 = { .x = pixman_double_to_fixed(buf->width + thickness(LIGHT) / 2.), .y = pixman_int_to_fixed(buf->height), }, }, }; pixman_rasterize_trapezoid(buf->pix, &trap, 0, 0); } static void draw_box_drawings_light_diagonal_cross(struct buf *buf) { draw_box_drawings_light_diagonal_upper_right_to_lower_left(buf); draw_box_drawings_light_diagonal_upper_left_to_lower_right(buf); } static void draw_box_drawings_light_left(struct buf *buf) { hline_middle_left(LIGHT); } static void draw_box_drawings_light_up(struct buf *buf) { vline_middle_up(LIGHT); } static void draw_box_drawings_light_right(struct buf *buf) { hline_middle_right(LIGHT); } static void draw_box_drawings_light_down(struct buf *buf) { vline_middle_down(LIGHT); } static void draw_box_drawings_heavy_left(struct buf *buf) { hline_middle_left(HEAVY); } static void draw_box_drawings_heavy_up(struct buf *buf) { vline_middle_up(HEAVY); } static void draw_box_drawings_heavy_right(struct buf *buf) { hline_middle_right(HEAVY); } static void draw_box_drawings_heavy_down(struct buf *buf) { vline_middle_down(HEAVY); } static void draw_box_drawings_light_left_and_heavy_right(struct buf *buf) { hline_middle_left(LIGHT); hline_middle_right(HEAVY); } static void draw_box_drawings_light_up_and_heavy_down(struct buf *buf) { vline_middle_up(LIGHT); vline_middle_down(HEAVY); } static void draw_box_drawings_heavy_left_and_light_right(struct buf *buf) { hline_middle_left(HEAVY); hline_middle_right(LIGHT); } static void draw_box_drawings_heavy_up_and_light_down(struct buf *buf) { vline_middle_up(HEAVY); vline_middle_down(LIGHT); } static void draw_upper_half_block(struct buf *buf) { rect(0, 0, buf->width, round(buf->height / 2.)); } static void draw_lower_one_eighth_block(struct buf *buf) { rect(0, buf->height - round(buf->height / 8.), buf->width, buf->height); } static void draw_lower_one_quarter_block(struct buf *buf) { rect(0, buf->height - round(buf->height / 4.), buf->width, buf->height); } static void draw_lower_three_eighths_block(struct buf *buf) { rect(0, buf->height - round(3. * buf->height / 8.), buf->width, buf->height); } static void draw_lower_half_block(struct buf *buf) { rect(0, buf->height - round(buf->height / 2.), buf->width, buf->height); } static void draw_lower_five_eighths_block(struct buf *buf) { rect(0, buf->height - round(5. * buf->height / 8.), buf->width, buf->height); } static void draw_lower_three_quarters_block(struct buf *buf) { rect(0, buf->height - round(3. * buf->height / 4.), buf->width, buf->height); } static void draw_lower_seven_eighths_block(struct buf *buf) { rect(0, buf->height - round(7. * buf->height / 8.), buf->width, buf->height); } static void draw_upper_one_quarter_block(struct buf *buf) { rect(0, 0, buf->width, round(buf->height / 4.)); } static void draw_upper_three_eighths_block(struct buf *buf) { rect(0, 0, buf->width, round(3. * buf->height / 8.)); } static void draw_upper_five_eighths_block(struct buf *buf) { rect(0, 0, buf->width, round(5. * buf->height / 8.)); } static void draw_upper_three_quarters_block(struct buf *buf) { rect(0, 0, buf->width, round(3. * buf->height / 4.)); } static void draw_upper_seven_eighths_block(struct buf *buf) { rect(0, 0, buf->width, round(7. * buf->height / 8.)); } static void draw_full_block(struct buf *buf) { rect(0, 0, buf->width, buf->height); } static void draw_left_seven_eighths_block(struct buf *buf) { rect(0, 0, round(7. * buf->width / 8.), buf->height); } static void draw_left_three_quarters_block(struct buf *buf) { rect(0, 0, round(3. * buf->width / 4.), buf->height); } static void draw_left_five_eighths_block(struct buf *buf) { rect(0, 0, round(5. * buf->width / 8.), buf->height); } static void draw_left_half_block(struct buf *buf) { rect(0, 0, round(buf->width / 2.), buf->height); } static void draw_left_three_eighths_block(struct buf *buf) { rect(0, 0, round(3. * buf->width / 8.), buf->height); } static void draw_left_one_quarter_block(struct buf *buf) { rect(0, 0, round(buf->width / 4.), buf->height); } static void draw_vertical_one_eighth_block_n(struct buf *buf, int n) { double x = round((double)n * buf->width / 8.); double w = round(buf->width / 8.); rect(x, 0, x + w, buf->height); } static void draw_left_one_eighth_block(struct buf *buf) { draw_vertical_one_eighth_block_n(buf, 0); } static void draw_vertical_one_eighth_block_2(struct buf *buf) { draw_vertical_one_eighth_block_n(buf, 1); } static void draw_vertical_one_eighth_block_3(struct buf *buf) { draw_vertical_one_eighth_block_n(buf, 2); } static void draw_vertical_one_eighth_block_4(struct buf *buf) { draw_vertical_one_eighth_block_n(buf, 3); } static void draw_vertical_one_eighth_block_5(struct buf *buf) { draw_vertical_one_eighth_block_n(buf, 4); } static void draw_vertical_one_eighth_block_6(struct buf *buf) { draw_vertical_one_eighth_block_n(buf, 5); } static void draw_vertical_one_eighth_block_7(struct buf *buf) { draw_vertical_one_eighth_block_n(buf, 6); } static void draw_right_half_block(struct buf *buf) { rect(round(buf->width / 2.), 0, buf->width, buf->height); } static void NOINLINE draw_pixman_shade(struct buf *buf, uint16_t v) { pixman_color_t shade = {.red = 0, .green = 0, .blue = 0, .alpha = v}; pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix, &shade, 1, (pixman_rectangle16_t []){{0, 0, buf->width, buf->height}}); } static void draw_light_shade(struct buf *buf) { pixman_format_code_t fmt = buf->format; if (buf->solid_shades && fmt == PIXMAN_a1) change_buffer_format(buf, PIXMAN_a8); else if (!buf->solid_shades && fmt == PIXMAN_a8) change_buffer_format(buf, PIXMAN_a1); if (buf->solid_shades) draw_pixman_shade(buf, 0x4000); else { for (size_t row = 0; row < buf->height; row += 2) { for (size_t col = 0; col < buf->width; col += 2) { size_t idx = col / 8; size_t bit_no = col % 8; set_a1_bit(buf->data, row * buf->stride + idx, bit_no); } } } } static void draw_medium_shade(struct buf *buf) { pixman_format_code_t fmt = buf->format; if (buf->solid_shades && fmt == PIXMAN_a1) change_buffer_format(buf, PIXMAN_a8); else if (!buf->solid_shades && fmt == PIXMAN_a8) change_buffer_format(buf, PIXMAN_a1); if (buf->solid_shades) draw_pixman_shade(buf, 0x8000); else { for (size_t row = 0; row < buf->height; row++) { for (size_t col = row % 2; col < buf->width; col += 2) { size_t idx = col / 8; size_t bit_no = col % 8; set_a1_bit(buf->data, row * buf->stride + idx, bit_no); } } } } static void draw_dark_shade(struct buf *buf) { pixman_format_code_t fmt = buf->format; if (buf->solid_shades && fmt == PIXMAN_a1) change_buffer_format(buf, PIXMAN_a8); else if (!buf->solid_shades && fmt == PIXMAN_a8) change_buffer_format(buf, PIXMAN_a1); if (buf->solid_shades) draw_pixman_shade(buf, 0xc000); else { for (size_t row = 0; row < buf->height; row++) { for (size_t col = 0; col < buf->width; col += 1 + row % 2) { size_t idx = col / 8; size_t bit_no = col % 8; set_a1_bit(buf->data, row * buf->stride + idx, bit_no); } } } } static void NOINLINE draw_horizontal_one_eighth_block_n(struct buf *buf, int n) { double y = round((double)n * buf->height / 8.); double h = round(buf->height / 8.); rect(0, y, buf->width, y + h); } static void draw_upper_one_eighth_block(struct buf *buf) { draw_horizontal_one_eighth_block_n(buf, 0); } static void draw_horizontal_one_eighth_block_2(struct buf *buf) { draw_horizontal_one_eighth_block_n(buf, 1); } static void draw_horizontal_one_eighth_block_3(struct buf *buf) { draw_horizontal_one_eighth_block_n(buf, 2); } static void draw_horizontal_one_eighth_block_4(struct buf *buf) { draw_horizontal_one_eighth_block_n(buf, 3); } static void draw_horizontal_one_eighth_block_5(struct buf *buf) { draw_horizontal_one_eighth_block_n(buf, 4); } static void draw_horizontal_one_eighth_block_6(struct buf *buf) { draw_horizontal_one_eighth_block_n(buf, 5); } static void draw_horizontal_one_eighth_block_7(struct buf *buf) { draw_horizontal_one_eighth_block_n(buf, 6); } static void draw_right_one_eighth_block(struct buf *buf) { rect(buf->width - round(buf->width / 8.), 0, buf->width, buf->height); } static void quad_upper_left(struct buf *buf) { rect(0, 0, ceil(buf->width / 2.), ceil(buf->height / 2.)); } static void quad_upper_right(struct buf *buf) { rect(floor(buf->width / 2.), 0, buf->width, ceil(buf->height / 2.)); } static void quad_lower_left(struct buf *buf) { rect(0, floor(buf->height / 2.), ceil(buf->width / 2.), buf->height); } static void quad_lower_right(struct buf *buf) { rect(floor(buf->width / 2.), floor(buf->height / 2.), buf->width, buf->height); } static void NOINLINE draw_quadrant(struct buf *buf, char32_t wc) { enum { UPPER_LEFT = 1 << 0, UPPER_RIGHT = 1 << 1, LOWER_LEFT = 1 << 2, LOWER_RIGHT = 1 << 3, }; static const uint8_t matrix[10] = { LOWER_LEFT, LOWER_RIGHT, UPPER_LEFT, UPPER_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | LOWER_RIGHT, UPPER_RIGHT, UPPER_RIGHT | LOWER_LEFT, UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, }; xassert(wc >= 0x2596 && wc <= 0x259f); const size_t idx = wc - 0x2596; xassert(idx < ALEN(matrix)); uint8_t encoded = matrix[idx]; if (encoded & UPPER_LEFT) quad_upper_left(buf); if (encoded & UPPER_RIGHT) quad_upper_right(buf); if (encoded & LOWER_LEFT) quad_lower_left(buf); if (encoded & LOWER_RIGHT) quad_lower_right(buf); } static void NOINLINE draw_braille(struct buf *buf, char32_t wc) { int w = min(buf->width / 4, buf->height / 8); int x_spacing = buf->width / 4; int y_spacing = buf->height / 8; int x_margin = x_spacing / 2; int y_margin = y_spacing / 2; int x_px_left = buf->width - 2 * x_margin - x_spacing - 2 * w; int y_px_left = buf->height - 2 * y_margin - 3 * y_spacing - 4 * w; LOG_DBG( "braille: before adjusting: " "cell: %dx%d, margin=%dx%d, spacing=%dx%d, width=%d, left=%dx%d", buf->width, buf->height, x_margin, y_margin, x_spacing, y_spacing, w, x_px_left, y_px_left); /* First, try hard to ensure the DOT width is non-zero */ if (x_px_left >= 2 && y_px_left >= 4 && w == 0) { w++; x_px_left -= 2; y_px_left -= 4; } /* Second, prefer a non-zero margin */ if (x_px_left >= 2 && x_margin == 0) { x_margin = 1; x_px_left -= 2; } if (y_px_left >= 2 && y_margin == 0) { y_margin = 1; y_px_left -= 2; } /* Third, increase spacing */ if (x_px_left >= 1) { x_spacing++; x_px_left--; } if (y_px_left >= 3) { y_spacing++; y_px_left -= 3; } /* Fourth, margins ("spacing", but on the sides) */ if (x_px_left >= 2) { x_margin++; x_px_left -= 2; } if (y_px_left >= 2) { y_margin++; y_px_left -= 2; } /* Last - increase dot width */ if (x_px_left >= 2 && y_px_left >= 4) { w++; x_px_left -= 2; y_px_left -= 4; } LOG_DBG( "braille: after adjusting: " "cell: %dx%d, margin=%dx%d, spacing=%dx%d, width=%d, left=%dx%d", buf->width, buf->height, x_margin, y_margin, x_spacing, y_spacing, w, x_px_left, y_px_left); xassert(x_px_left <= 1 || y_px_left <= 1); xassert(2 * x_margin + 2 * w + x_spacing <= buf->width); xassert(2 * y_margin + 4 * w + 3 * y_spacing <= buf->height); int x[2], y[4]; x[0] = x_margin; x[1] = x_margin + w + x_spacing; y[0] = y_margin; y[1] = y[0] + w + y_spacing; y[2] = y[1] + w + y_spacing; y[3] = y[2] + w + y_spacing; assert(wc >= 0x2800); assert(wc <= 0x28ff); uint8_t sym = wc - 0x2800; /* Left side */ if (sym & 1) rect(x[0], y[0], x[0] + w, y[0] + w); if (sym & 2) rect(x[0], y[1], x[0] + w, y[1] + w); if (sym & 4) rect(x[0], y[2], x[0] + w, y[2] + w); /* Right side */ if (sym & 8) rect(x[1], y[0], x[1] + w, y[0] + w); if (sym & 16) rect(x[1], y[1], x[1] + w, y[1] + w); if (sym & 32) rect(x[1], y[2], x[1] + w, y[2] + w); /* 8-dot patterns */ if (sym & 64) rect(x[0], y[3], x[0] + w, y[3] + w); if (sym & 128) rect(x[1], y[3], x[1] + w, y[3] + w); } static void sextant_upper_left(struct buf *buf) { rect(0, 0, buf->x_halfs[0], buf->y_thirds[0]); } static void sextant_middle_left(struct buf *buf) { rect(0, buf->y_thirds[0], buf->x_halfs[0], buf->y_thirds[1]); } static void sextant_lower_left(struct buf *buf) { rect(0, buf->y_thirds[1], buf->x_halfs[0], buf->height); } static void sextant_upper_right(struct buf *buf) { rect(buf->x_halfs[1], 0, buf->width, buf->y_thirds[0]); } static void sextant_middle_right(struct buf *buf) { rect(buf->x_halfs[1], buf->y_thirds[0], buf->width, buf->y_thirds[1]); } static void sextant_lower_right(struct buf *buf) { rect(buf->x_halfs[1], buf->y_thirds[1], buf->width, buf->height); } static void NOINLINE draw_sextant(struct buf *buf, char32_t wc) { /* * Each byte encodes one sextant: * * Bit sextant * 0 upper left * 1 middle left * 2 lower left * 3 upper right * 4 middle right * 5 lower right */ enum { UPPER_LEFT = 1 << 0, MIDDLE_LEFT = 1 << 1, LOWER_LEFT = 1 << 2, UPPER_RIGHT = 1 << 3, MIDDLE_RIGHT = 1 << 4, LOWER_RIGHT = 1 << 5, }; /* TODO: move this to a separate file? */ static const uint8_t matrix[60] = { /* U+1fb00 - U+1fb0f */ UPPER_LEFT, UPPER_RIGHT, UPPER_LEFT | UPPER_RIGHT, MIDDLE_LEFT, UPPER_LEFT | MIDDLE_LEFT, UPPER_RIGHT | MIDDLE_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT, MIDDLE_RIGHT, UPPER_LEFT | MIDDLE_RIGHT, UPPER_RIGHT | MIDDLE_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_RIGHT, MIDDLE_LEFT | MIDDLE_RIGHT, UPPER_LEFT | MIDDLE_LEFT | MIDDLE_RIGHT, UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT, LOWER_LEFT, /* U+1fb10 - U+1fb1f */ UPPER_LEFT | LOWER_LEFT, UPPER_RIGHT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT, MIDDLE_LEFT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_LEFT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | LOWER_LEFT, MIDDLE_RIGHT | LOWER_LEFT, UPPER_LEFT | MIDDLE_RIGHT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_RIGHT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_RIGHT | LOWER_LEFT, MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT, UPPER_LEFT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT, LOWER_RIGHT, UPPER_LEFT | LOWER_RIGHT, /* U+1fb20 - U+1fb2f */ UPPER_RIGHT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | LOWER_RIGHT, MIDDLE_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | LOWER_RIGHT, MIDDLE_RIGHT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_RIGHT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_RIGHT | LOWER_RIGHT, MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_RIGHT, LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, /* U+1fb30 - U+1fb3b */ UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, }; xassert(wc >= 0x1fb00 && wc <= 0x1fb3b); const size_t idx = wc - 0x1fb00; xassert(idx < ALEN(matrix)); uint8_t encoded = matrix[idx]; if (encoded & UPPER_LEFT) sextant_upper_left(buf); if (encoded & MIDDLE_LEFT) sextant_middle_left(buf); if (encoded & LOWER_LEFT) sextant_lower_left(buf); if (encoded & UPPER_RIGHT) sextant_upper_right(buf); if (encoded & MIDDLE_RIGHT) sextant_middle_right(buf); if (encoded & LOWER_RIGHT) sextant_lower_right(buf); } static void octant_upper_left(struct buf *buf) { rect(0, 0, buf->x_halfs[0], buf->y_quads[0]); } static void octant_middle_up_left(struct buf *buf) { rect(0, buf->y_quads[0], buf->x_halfs[0], buf->y_quads[1]); } static void octant_middle_down_left(struct buf *buf) { rect(0, buf->y_quads[1], buf->x_halfs[0], buf->y_quads[2]); } static void octant_lower_left(struct buf *buf) { rect(0, buf->y_quads[2], buf->x_halfs[0], buf->height); } static void octant_upper_right(struct buf *buf) { rect(buf->x_halfs[1], 0, buf->width, buf->y_quads[0]); } static void octant_middle_up_right(struct buf *buf) { rect(buf->x_halfs[1], buf->y_quads[0], buf->width, buf->y_quads[1]); } static void octant_middle_down_right(struct buf *buf) { rect(buf->x_halfs[1], buf->y_quads[1], buf->width, buf->y_quads[2]); } static void octant_lower_right(struct buf *buf) { rect(buf->x_halfs[1], buf->y_quads[2], buf->width, buf->height); } static void NOINLINE draw_octant(struct buf *buf, char32_t wc) { /* * Each byte encodes one octant: * * Bit octant part * 0 upper left * 1 middle, upper left * 2 middle, lower left * 3 lower, left * 4 upper right * 5 middle, upper right * 6 middle, lower right * 7 lower right */ enum { UPPER_LEFT = 1 << 0, MIDDLE_UP_LEFT = 1 << 1, MIDDLE_DOWN_LEFT = 1 << 2, LOWER_LEFT = 1 << 3, UPPER_RIGHT = 1 << 4, MIDDLE_UP_RIGHT = 1 << 5, MIDDLE_DOWN_RIGHT = 1 << 6, LOWER_RIGHT = 1 << 7, }; /* TODO: move this to a separate file */ static const uint8_t matrix[230] = { /* U+1CD00 - U+1CD0F */ MIDDLE_UP_LEFT, MIDDLE_UP_LEFT | UPPER_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | UPPER_RIGHT, MIDDLE_UP_RIGHT, UPPER_LEFT | MIDDLE_UP_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT, MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT, MIDDLE_DOWN_LEFT, UPPER_LEFT | MIDDLE_DOWN_LEFT, UPPER_RIGHT | MIDDLE_DOWN_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT, /* U+1CD10 - U+1CD1F */ MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, MIDDLE_DOWN_RIGHT, UPPER_LEFT | MIDDLE_DOWN_RIGHT, UPPER_RIGHT | MIDDLE_DOWN_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT, MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, /* U+1CD20 - U+1CD2F */ UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, /* U+1CD30 - U+1CD3F */ UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, UPPER_LEFT | LOWER_LEFT, UPPER_RIGHT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT, MIDDLE_UP_LEFT | LOWER_LEFT, UPPER_LEFT | MIDDLE_UP_LEFT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT, MIDDLE_UP_RIGHT | LOWER_LEFT, UPPER_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT, /* U+1CD40 - U+1CD4F */ UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT, MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, UPPER_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, /* U+1CD50 - U+1CD5F */ UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, /* U+1CD60 - U+1CD6F */ UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, /* U+1CD70 - U+1CD7F */ UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, UPPER_LEFT | LOWER_RIGHT, UPPER_RIGHT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | LOWER_RIGHT, MIDDLE_UP_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_RIGHT, MIDDLE_UP_RIGHT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_RIGHT, MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, /* U+1CD80 - U+1CD8F */ MIDDLE_DOWN_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, /* U+1CD90 - U+1CD9F */ UPPER_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, /* U+1CDA0 - U+1CDAF */ MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, UPPER_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, /* U+1CDB0 - U+1CDBF */ UPPER_LEFT | MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, /* U+1CDC0 - U+1CDCF */ UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, /* U+1CDD0 - U+1CDDF */ UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, /* U+1CDE0 - U+1CDE5 */ UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, }; _Static_assert(ALEN(matrix) == 230, "incorrect number of codepoints"); #if defined(_DEBUG) const size_t last_implemented = 0x1cde5; for (size_t i = 0; i < sizeof(matrix) / sizeof(matrix[0]); i++) { if (i + 0x1cd00 > last_implemented) break; for (size_t j = 0; j < sizeof(matrix) / sizeof(matrix[0]); j++) { if (j + 0x1cd00 > last_implemented) break; if (i == j) continue; if (matrix[i] == matrix[j]) { BUG("octant U+%05x (idx=%zu) is the same as U+%05x (idx=%zu)", matrix[i], i, matrix[j], j); } } } #endif xassert(wc >= 0x1cd00 && wc <= 0x1cde5); const size_t idx = wc - 0x1cd00; xassert(idx < ALEN(matrix)); uint8_t encoded = matrix[idx]; if (encoded & UPPER_LEFT) octant_upper_left(buf); if (encoded & MIDDLE_UP_LEFT) octant_middle_up_left(buf); if (encoded & MIDDLE_DOWN_LEFT) octant_middle_down_left(buf); if (encoded & LOWER_LEFT) octant_lower_left(buf); if (encoded & UPPER_RIGHT) octant_upper_right(buf); if (encoded & MIDDLE_UP_RIGHT) octant_middle_up_right(buf); if (encoded & MIDDLE_DOWN_RIGHT) octant_middle_down_right(buf); if (encoded & LOWER_RIGHT) octant_lower_right(buf); } static void NOINLINE draw_wedge_triangle(struct buf *buf, char32_t wc) { const int width = buf->width; const int height = buf->height; int halfs0 = buf->x_halfs[0]; int halfs1 = buf->x_halfs[1]; int thirds0 = buf->y_thirds[0]; int thirds1 = buf->y_thirds[1]; int p1_x, p1_y, p2_x, p2_y, p3_x, p3_y; switch (wc) { case 0x1fb3c: /* 🬼 */ p1_x = p2_x = 0; p3_x = halfs0; p1_y = thirds1; p2_y = p3_y = height; break; case 0x1fb52: /* 🭒 */ p1_x = p2_x = 0; p3_x = halfs0; p1_y = thirds1; p2_y = p3_y = height; break; case 0x1fb3d: /* 🬽 */ p1_x = p2_x = 0; p3_x = width; p1_y = thirds1; p2_y = p3_y = height; break; case 0x1fb53: /* 🭓 */ p1_x = p2_x = 0; p3_x = width; p1_y = thirds1; p2_y = p3_y = height; break; case 0x1fb3e: /* 🬾 */ p1_x = p2_x = 0; p3_x = halfs0; p1_y = thirds0; p2_y = p3_y = height; break; case 0x1fb54: /* 🭔 */ p1_x = p2_x = 0; p3_x = halfs0; p1_y = thirds0; p2_y = p3_y = height; break; case 0x1fb3f: /* 🬿 */ p1_x = p2_x = 0; p3_x = width; p1_y = thirds0; p2_y = p3_y = height; break; case 0x1fb55: /* 🭕 */ p1_x = p2_x = 0; p3_x = width; p1_y = thirds0; p2_y = p3_y = height; break; case 0x1fb40: /* 🭀 */ case 0x1fb56: /* 🭖 */ p1_x = p2_x = 0; p3_x = halfs0; p1_y = 0; p2_y = p3_y = height; break; case 0x1fb47: /* 🭇 */ p1_x = p2_x = width; p3_x = halfs1; p1_y = thirds1; p2_y = p3_y = height; break; case 0x1fb5d: /* 🭝 */ p1_x = p2_x = width; p3_x = halfs1; p1_y = thirds1; p2_y = p3_y = height; break; case 0x1fb48: /* 🭈 */ p1_x = p2_x = width; p3_x = 0; p1_y = thirds1; p2_y = p3_y = height; break; case 0x1fb5e: /* 🭞 */ p1_x = p2_x = width; p3_x = 0; p1_y = thirds1; p2_y = p3_y = height; break; case 0x1fb49: /* 🭉 */ p1_x = p2_x = width; p3_x = halfs1; p1_y = thirds0; p2_y = p3_y = height; break; case 0x1fb5f: /* 🭟 */ p1_x = p2_x = width; p3_x = halfs1; p1_y = thirds0; p2_y = p3_y = height; break; case 0x1fb4a: /* 🭊 */ p1_x = p2_x = width; p3_x = 0; p1_y = thirds0; p2_y = p3_y = height; break; case 0x1fb60: /* 🭠 */ p1_x = p2_x = width; p3_x = 0; p1_y = thirds0; p2_y = p3_y = height; break; case 0x1fb4b: /* 🭋 */ case 0x1fb61: /* 🭡 */ p1_x = p2_x = width; p3_x = halfs1; p1_y = 0; p2_y = p3_y = height; break; case 0x1fb57: /* 🭗 */ p1_x = p2_x = 0; p3_x = halfs0; p1_y = p3_y = 0; p2_y = thirds0; break; case 0x1fb41: /* 🭁 */ p1_x = p2_x = 0; p3_x = halfs0; p1_y = p3_y = 0; p2_y = thirds0; break; case 0x1fb58: /* 🭘 */ p1_x = p2_x = 0; p3_x = width; p1_y = p3_y = 0; p2_y = thirds0; break; case 0x1fb42: /* 🭂 */ p1_x = p2_x = 0; p3_x = width; p1_y = p3_y = 0; p2_y = thirds0; break; case 0x1fb59: /* 🭙 */ p1_x = p2_x = 0; p3_x = halfs0; p1_y = p3_y = 0; p2_y = thirds1; break; case 0x1fb43: /* 🭃 */ p1_x = p2_x = 0; p3_x = halfs0; p1_y = p3_y = 0; p2_y = thirds1; break; case 0x1fb5a: /* 🭚 */ p1_x = p2_x = 0; p3_x = width; p1_y = p3_y = 0; p2_y = thirds1; break; case 0x1fb44: /* 🭄 */ p1_x = p2_x = 0; p3_x = width; p1_y = p3_y = 0; p2_y = thirds1; break; case 0x1fb5b: /* 🭛 */ case 0x1fb45: /* 🭅 */ p1_x = p2_x = 0; p3_x = halfs0; p1_y = p3_y = 0; p2_y = height; break; case 0x1fb62: /* 🭢 */ p1_x = p2_x = width; p3_x = halfs1; p1_y = p3_y = 0; p2_y = thirds0; break; case 0x1fb4c: /* 🭌 */ p1_x = p2_x = width; p3_x = halfs1; p1_y = p3_y = 0; p2_y = thirds0; break; case 0x1fb63: /* 🭣 */ p1_x = p2_x = width; p3_x = 0; p1_y = p3_y = 0; p2_y = thirds0; break; case 0x1fb4d: /* 🭍 */ p1_x = p2_x = width; p3_x = 0; p1_y = p3_y = 0; p2_y = thirds0; break; case 0x1fb64: /* 🭤 */ p1_x = p2_x = width; p3_x = halfs1; p1_y = p3_y = 0; p2_y = thirds1; break; case 0x1fb4e: /* 🭎 */ p1_x = p2_x = width; p3_x = halfs1; p1_y = p3_y = 0; p2_y = thirds1; break; case 0x1fb65: /* 🭥 */ p1_x = p2_x = width; p3_x = 0; p1_y = p3_y = 0; p2_y = thirds1; break; case 0x1fb4f: /* 🭏 */ p1_x = p2_x = width; p3_x = 0; p1_y = p3_y = 0; p2_y = thirds1; break; case 0x1fb66: /* 🭦 */ case 0x1fb50: /* 🭐 */ p1_x = p2_x = width; p3_x = halfs1; p1_y = p3_y = 0; p2_y = height; break; case 0x1fb46: /* 🭆 */ p1_x = 0; p1_y = thirds1; p2_x = width; p2_y = thirds0; p3_x = width; p3_y = p1_y; break; case 0x1fb51: /* 🭑 */ p1_x = 0; p1_y = thirds0; p2_x = 0; p2_y = thirds1; p3_x = width; p3_y = p2_y; break; case 0x1fb5c: /* 🭜 */ p1_x = 0; p1_y = thirds0; p2_x = 0; p2_y = thirds1; p3_x = width; p3_y = p1_y; break; case 0x1fb67: /* 🭧 */ p1_x = 0; p1_y = thirds0; p2_x = width; p2_y = p1_y; p3_x = width; p3_y = thirds1; break; case 0x1fb6c: /* 🭬 */ case 0x1fb68: /* 🭨 */ p1_x = 0; p1_y = 0; p2_x = halfs0; p2_y = height / 2; p3_x = 0; p3_y = height; break; case 0x1fb6d: /* 🭭 */ case 0x1fb69: /* 🭩 */ p1_x = 0; p1_y = 0; p2_x = halfs1; p2_y = height / 2; p3_x = width; p3_y = 0; break; case 0x1fb6e: /* 🭮 */ case 0x1fb6a: /* 🭪 */ p1_x = width; p1_y = 0; p2_x = halfs1; p2_y = height / 2; p3_x = width; p3_y = height; break; case 0x1fb6f: /* 🭯 */ case 0x1fb6b: /* 🭫 */ p1_x = 0; p1_y = height; p2_x = halfs1; p2_y = height / 2; p3_x = width; p3_y = height; break; default: BUG("unimplemented Unicode codepoint"); break; } const pixman_triangle_t tri = { .p1 = {.x = pixman_int_to_fixed(p1_x), .y = pixman_int_to_fixed(p1_y)}, .p2 = {.x = pixman_int_to_fixed(p2_x), .y = pixman_int_to_fixed(p2_y)}, .p3 = {.x = pixman_int_to_fixed(p3_x), .y = pixman_int_to_fixed(p3_y)}, }; pixman_image_t *src = pixman_image_create_solid_fill(&white); pixman_composite_triangles( PIXMAN_OP_OVER, src, buf->pix, buf->format, 0, 0, 0, 0, 1, &tri); pixman_image_unref(src); } static void NOINLINE draw_wedge_triangle_inverted(struct buf *buf, char32_t wc) { draw_wedge_triangle(buf, wc); pixman_image_t *src = pixman_image_create_solid_fill(&white); pixman_image_composite(PIXMAN_OP_OUT, src, NULL, buf->pix, 0, 0, 0, 0, 0, 0, buf->width, buf->height); pixman_image_unref(src); } static void NOINLINE draw_wedge_triangle_and_box(struct buf *buf, char32_t wc) { draw_wedge_triangle(buf, wc); const int width = buf->width; const int height = buf->height; pixman_box32_t box; switch (wc) { case 0x1fb46: case 0x1fb51: box = (pixman_box32_t){ .x1 = 0, .y1 = buf->y_thirds[1], .x2 = width, .y2 = height, }; break; case 0x1fb5c: case 0x1fb67: box = (pixman_box32_t){ .x1 = 0, .y1 = 0, .x2 = width, .y2 = buf->y_thirds[0], }; break; } pixman_image_fill_boxes(PIXMAN_OP_SRC, buf->pix, &white, 1, &box); } static void draw_left_and_lower_one_eighth_block(struct buf *buf) { draw_left_one_eighth_block(buf); draw_lower_one_eighth_block(buf); } static void draw_left_and_upper_one_eighth_block(struct buf *buf) { draw_left_one_eighth_block(buf); draw_upper_one_eighth_block(buf); } static void draw_right_and_upper_one_eighth_block(struct buf *buf) { draw_right_one_eighth_block(buf); draw_upper_one_eighth_block(buf); } static void draw_right_and_lower_one_eighth_block(struct buf *buf) { draw_right_one_eighth_block(buf); draw_lower_one_eighth_block(buf); } static void draw_upper_and_lower_one_eighth_block(struct buf *buf) { draw_upper_one_eighth_block(buf); draw_lower_one_eighth_block(buf); } static void draw_horizontal_one_eighth_1358_block(struct buf *buf) { draw_upper_one_eighth_block(buf); draw_horizontal_one_eighth_block_3(buf); draw_horizontal_one_eighth_block_5(buf); draw_lower_one_eighth_block(buf); } static void draw_right_one_quarter_block(struct buf *buf) { rect(buf->width - round(buf->width / 4.), 0, buf->width, buf->height); } static void draw_right_three_eighths_block(struct buf *buf) { rect(buf->width - round(3. * buf->width / 8.), 0, buf->width, buf->height); } static void draw_right_five_eighths_block(struct buf *buf) { rect(buf->width - round(5. * buf->width / 8.), 0, buf->width, buf->height); } static void draw_right_three_quarters_block(struct buf *buf) { rect(buf->width - round(3. * buf->width / 4.), 0, buf->width, buf->height); } static void draw_right_seven_eighths_block(struct buf *buf) { rect(buf->width - round(7. * buf->width / 8.), 0, buf->width, buf->height); } static void draw_glyph(struct buf *buf, char32_t wc) { IGNORE_WARNING("-Wpedantic") switch (wc) { case 0x2500: draw_box_drawings_light_horizontal(buf); break; case 0x2501: draw_box_drawings_heavy_horizontal(buf); break; case 0x2502: draw_box_drawings_light_vertical(buf); break; case 0x2503: draw_box_drawings_heavy_vertical(buf); break; case 0x2504: draw_box_drawings_light_triple_dash_horizontal(buf); break; case 0x2505: draw_box_drawings_heavy_triple_dash_horizontal(buf); break; case 0x2506: draw_box_drawings_light_triple_dash_vertical(buf); break; case 0x2507: draw_box_drawings_heavy_triple_dash_vertical(buf); break; case 0x2508: draw_box_drawings_light_quadruple_dash_horizontal(buf); break; case 0x2509: draw_box_drawings_heavy_quadruple_dash_horizontal(buf); break; case 0x250a: draw_box_drawings_light_quadruple_dash_vertical(buf); break; case 0x250b: draw_box_drawings_heavy_quadruple_dash_vertical(buf); break; case 0x250c: draw_box_drawings_light_down_and_right(buf); break; case 0x250d: draw_box_drawings_down_light_and_right_heavy(buf); break; case 0x250e: draw_box_drawings_down_heavy_and_right_light(buf); break; case 0x250f: draw_box_drawings_heavy_down_and_right(buf); break; case 0x2510: draw_box_drawings_light_down_and_left(buf); break; case 0x2511: draw_box_drawings_down_light_and_left_heavy(buf); break; case 0x2512: draw_box_drawings_down_heavy_and_left_light(buf); break; case 0x2513: draw_box_drawings_heavy_down_and_left(buf); break; case 0x2514: draw_box_drawings_light_up_and_right(buf); break; case 0x2515: draw_box_drawings_up_light_and_right_heavy(buf); break; case 0x2516: draw_box_drawings_up_heavy_and_right_light(buf); break; case 0x2517: draw_box_drawings_heavy_up_and_right(buf); break; case 0x2518: draw_box_drawings_light_up_and_left(buf); break; case 0x2519: draw_box_drawings_up_light_and_left_heavy(buf); break; case 0x251a: draw_box_drawings_up_heavy_and_left_light(buf); break; case 0x251b: draw_box_drawings_heavy_up_and_left(buf); break; case 0x251c: draw_box_drawings_light_vertical_and_right(buf); break; case 0x251d: draw_box_drawings_vertical_light_and_right_heavy(buf); break; case 0x251e: draw_box_drawings_up_heavy_and_right_down_light(buf); break; case 0x251f: draw_box_drawings_down_heavy_and_right_up_light(buf); break; case 0x2520: draw_box_drawings_vertical_heavy_and_right_light(buf); break; case 0x2521: draw_box_drawings_down_light_and_right_up_heavy(buf); break; case 0x2522: draw_box_drawings_up_light_and_right_down_heavy(buf); break; case 0x2523: draw_box_drawings_heavy_vertical_and_right(buf); break; case 0x2524: draw_box_drawings_light_vertical_and_left(buf); break; case 0x2525: draw_box_drawings_vertical_light_and_left_heavy(buf); break; case 0x2526: draw_box_drawings_up_heavy_and_left_down_light(buf); break; case 0x2527: draw_box_drawings_down_heavy_and_left_up_light(buf); break; case 0x2528: draw_box_drawings_vertical_heavy_and_left_light(buf); break; case 0x2529: draw_box_drawings_down_light_and_left_up_heavy(buf); break; case 0x252a: draw_box_drawings_up_light_and_left_down_heavy(buf); break; case 0x252b: draw_box_drawings_heavy_vertical_and_left(buf); break; case 0x252c: draw_box_drawings_light_down_and_horizontal(buf); break; case 0x252d: draw_box_drawings_left_heavy_and_right_down_light(buf); break; case 0x252e: draw_box_drawings_right_heavy_and_left_down_light(buf); break; case 0x252f: draw_box_drawings_down_light_and_horizontal_heavy(buf); break; case 0x2530: draw_box_drawings_down_heavy_and_horizontal_light(buf); break; case 0x2531: draw_box_drawings_right_light_and_left_down_heavy(buf); break; case 0x2532: draw_box_drawings_left_light_and_right_down_heavy(buf); break; case 0x2533: draw_box_drawings_heavy_down_and_horizontal(buf); break; case 0x2534: draw_box_drawings_light_up_and_horizontal(buf); break; case 0x2535: draw_box_drawings_left_heavy_and_right_up_light(buf); break; case 0x2536: draw_box_drawings_right_heavy_and_left_up_light(buf); break; case 0x2537: draw_box_drawings_up_light_and_horizontal_heavy(buf); break; case 0x2538: draw_box_drawings_up_heavy_and_horizontal_light(buf); break; case 0x2539: draw_box_drawings_right_light_and_left_up_heavy(buf); break; case 0x253a: draw_box_drawings_left_light_and_right_up_heavy(buf); break; case 0x253b: draw_box_drawings_heavy_up_and_horizontal(buf); break; case 0x253c: draw_box_drawings_light_vertical_and_horizontal(buf); break; case 0x253d: draw_box_drawings_left_heavy_and_right_vertical_light(buf); break; case 0x253e: draw_box_drawings_right_heavy_and_left_vertical_light(buf); break; case 0x253f: draw_box_drawings_vertical_light_and_horizontal_heavy(buf); break; case 0x2540: draw_box_drawings_up_heavy_and_down_horizontal_light(buf); break; case 0x2541: draw_box_drawings_down_heavy_and_up_horizontal_light(buf); break; case 0x2542: draw_box_drawings_vertical_heavy_and_horizontal_light(buf); break; case 0x2543: draw_box_drawings_left_up_heavy_and_right_down_light(buf); break; case 0x2544: draw_box_drawings_right_up_heavy_and_left_down_light(buf); break; case 0x2545: draw_box_drawings_left_down_heavy_and_right_up_light(buf); break; case 0x2546: draw_box_drawings_right_down_heavy_and_left_up_light(buf); break; case 0x2547: draw_box_drawings_down_light_and_up_horizontal_heavy(buf); break; case 0x2548: draw_box_drawings_up_light_and_down_horizontal_heavy(buf); break; case 0x2549: draw_box_drawings_right_light_and_left_vertical_heavy(buf); break; case 0x254a: draw_box_drawings_left_light_and_right_vertical_heavy(buf); break; case 0x254b: draw_box_drawings_heavy_vertical_and_horizontal(buf); break; case 0x254c: draw_box_drawings_light_double_dash_horizontal(buf); break; case 0x254d: draw_box_drawings_heavy_double_dash_horizontal(buf); break; case 0x254e: draw_box_drawings_light_double_dash_vertical(buf); break; case 0x254f: draw_box_drawings_heavy_double_dash_vertical(buf); break; case 0x2550: draw_box_drawings_double_horizontal(buf); break; case 0x2551: draw_box_drawings_double_vertical(buf); break; case 0x2552: draw_box_drawings_down_single_and_right_double(buf); break; case 0x2553: draw_box_drawings_down_double_and_right_single(buf); break; case 0x2554: draw_box_drawings_double_down_and_right(buf); break; case 0x2555: draw_box_drawings_down_single_and_left_double(buf); break; case 0x2556: draw_box_drawings_down_double_and_left_single(buf); break; case 0x2557: draw_box_drawings_double_down_and_left(buf); break; case 0x2558: draw_box_drawings_up_single_and_right_double(buf); break; case 0x2559: draw_box_drawings_up_double_and_right_single(buf); break; case 0x255a: draw_box_drawings_double_up_and_right(buf); break; case 0x255b: draw_box_drawings_up_single_and_left_double(buf); break; case 0x255c: draw_box_drawings_up_double_and_left_single(buf); break; case 0x255d: draw_box_drawings_double_up_and_left(buf); break; case 0x255e: draw_box_drawings_vertical_single_and_right_double(buf); break; case 0x255f: draw_box_drawings_vertical_double_and_right_single(buf); break; case 0x2560: draw_box_drawings_double_vertical_and_right(buf); break; case 0x2561: draw_box_drawings_vertical_single_and_left_double(buf); break; case 0x2562: draw_box_drawings_vertical_double_and_left_single(buf); break; case 0x2563: draw_box_drawings_double_vertical_and_left(buf); break; case 0x2564: draw_box_drawings_down_single_and_horizontal_double(buf); break; case 0x2565: draw_box_drawings_down_double_and_horizontal_single(buf); break; case 0x2566: draw_box_drawings_double_down_and_horizontal(buf); break; case 0x2567: draw_box_drawings_up_single_and_horizontal_double(buf); break; case 0x2568: draw_box_drawings_up_double_and_horizontal_single(buf); break; case 0x2569: draw_box_drawings_double_up_and_horizontal(buf); break; case 0x256a: draw_box_drawings_vertical_single_and_horizontal_double(buf); break; case 0x256b: draw_box_drawings_vertical_double_and_horizontal_single(buf); break; case 0x256c: draw_box_drawings_double_vertical_and_horizontal(buf); break; case 0x256d ... 0x2570: draw_box_drawings_light_arc(buf, wc); break; case 0x2571: draw_box_drawings_light_diagonal_upper_right_to_lower_left(buf); break; case 0x2572: draw_box_drawings_light_diagonal_upper_left_to_lower_right(buf); break; case 0x2573: draw_box_drawings_light_diagonal_cross(buf); break; case 0x2574: draw_box_drawings_light_left(buf); break; case 0x2575: draw_box_drawings_light_up(buf); break; case 0x2576: draw_box_drawings_light_right(buf); break; case 0x2577: draw_box_drawings_light_down(buf); break; case 0x2578: draw_box_drawings_heavy_left(buf); break; case 0x2579: draw_box_drawings_heavy_up(buf); break; case 0x257a: draw_box_drawings_heavy_right(buf); break; case 0x257b: draw_box_drawings_heavy_down(buf); break; case 0x257c: draw_box_drawings_light_left_and_heavy_right(buf); break; case 0x257d: draw_box_drawings_light_up_and_heavy_down(buf); break; case 0x257e: draw_box_drawings_heavy_left_and_light_right(buf); break; case 0x257f: draw_box_drawings_heavy_up_and_light_down(buf); break; case 0x2580: draw_upper_half_block(buf); break; case 0x2581: draw_lower_one_eighth_block(buf); break; case 0x2582: draw_lower_one_quarter_block(buf); break; case 0x2583: draw_lower_three_eighths_block(buf); break; case 0x2584: draw_lower_half_block(buf); break; case 0x2585: draw_lower_five_eighths_block(buf); break; case 0x2586: draw_lower_three_quarters_block(buf); break; case 0x2587: draw_lower_seven_eighths_block(buf); break; case 0x2588: draw_full_block(buf); break; case 0x2589: draw_left_seven_eighths_block(buf); break; case 0x258a: draw_left_three_quarters_block(buf); break; case 0x258b: draw_left_five_eighths_block(buf); break; case 0x258c: draw_left_half_block(buf); break; case 0x258d: draw_left_three_eighths_block(buf); break; case 0x258e: draw_left_one_quarter_block(buf); break; case 0x258f: draw_left_one_eighth_block(buf); break; case 0x2590: draw_right_half_block(buf); break; case 0x2591: draw_light_shade(buf); break; case 0x2592: draw_medium_shade(buf); break; case 0x2593: draw_dark_shade(buf); break; case 0x2594: draw_upper_one_eighth_block(buf); break; case 0x2595: draw_right_one_eighth_block(buf); break; case 0x2596 ... 0x259f: draw_quadrant(buf, wc); break; case 0x2800 ... 0x28ff: draw_braille(buf, wc); break; case 0x1cd00 ... 0x1cde5: draw_octant(buf, wc); break; case 0x1fb00 ... 0x1fb3b: draw_sextant(buf, wc); break; case 0x1fb3c ... 0x1fb40: case 0x1fb47 ... 0x1fb4b: case 0x1fb57 ... 0x1fb5b: case 0x1fb62 ... 0x1fb66: case 0x1fb6c ... 0x1fb6f: draw_wedge_triangle(buf, wc); break; case 0x1fb41 ... 0x1fb45: case 0x1fb4c ... 0x1fb50: case 0x1fb52 ... 0x1fb56: case 0x1fb5d ... 0x1fb61: case 0x1fb68 ... 0x1fb6b: draw_wedge_triangle_inverted(buf, wc); break; case 0x1fb46: case 0x1fb51: case 0x1fb5c: case 0x1fb67: draw_wedge_triangle_and_box(buf, wc); break; case 0x1fb9a: draw_wedge_triangle(buf, 0x1fb6d); draw_wedge_triangle(buf, 0x1fb6f); break; case 0x1fb9b: draw_wedge_triangle(buf, 0x1fb6c); draw_wedge_triangle(buf, 0x1fb6e); break; case 0x1fb70: draw_vertical_one_eighth_block_2(buf); break; case 0x1fb71: draw_vertical_one_eighth_block_3(buf); break; case 0x1fb72: draw_vertical_one_eighth_block_4(buf); break; case 0x1fb73: draw_vertical_one_eighth_block_5(buf); break; case 0x1fb74: draw_vertical_one_eighth_block_6(buf); break; case 0x1fb75: draw_vertical_one_eighth_block_7(buf); break; case 0x1fb76: draw_horizontal_one_eighth_block_2(buf); break; case 0x1fb77: draw_horizontal_one_eighth_block_3(buf); break; case 0x1fb78: draw_horizontal_one_eighth_block_4(buf); break; case 0x1fb79: draw_horizontal_one_eighth_block_5(buf); break; case 0x1fb7a: draw_horizontal_one_eighth_block_6(buf); break; case 0x1fb7b: draw_horizontal_one_eighth_block_7(buf); break; case 0x1fb82: draw_upper_one_quarter_block(buf); break; case 0x1fb83: draw_upper_three_eighths_block(buf); break; case 0x1fb84: draw_upper_five_eighths_block(buf); break; case 0x1fb85: draw_upper_three_quarters_block(buf); break; case 0x1fb86: draw_upper_seven_eighths_block(buf); break; case 0x1fb7c: draw_left_and_lower_one_eighth_block(buf); break; case 0x1fb7d: draw_left_and_upper_one_eighth_block(buf); break; case 0x1fb7e: draw_right_and_upper_one_eighth_block(buf); break; case 0x1fb7f: draw_right_and_lower_one_eighth_block(buf); break; case 0x1fb80: draw_upper_and_lower_one_eighth_block(buf); break; case 0x1fb81: draw_horizontal_one_eighth_1358_block(buf); break; case 0x1fb87: draw_right_one_quarter_block(buf); break; case 0x1fb88: draw_right_three_eighths_block(buf); break; case 0x1fb89: draw_right_five_eighths_block(buf); break; case 0x1fb8a: draw_right_three_quarters_block(buf); break; case 0x1fb8b: draw_right_seven_eighths_block(buf); break; } UNIGNORE_WARNINGS } struct fcft_glyph * COLD box_drawing(const struct terminal *term, char32_t wc) { int width = term->cell_width; int height = term->cell_height; pixman_format_code_t fmt = term->fonts[0]->antialias ? PIXMAN_a8 : PIXMAN_a1; int stride = stride_for_format_and_width(fmt, width); uint8_t *data = xcalloc(height * stride, 1); pixman_image_t *pix = pixman_image_create_bits_no_clear( fmt, width, height, (uint32_t*)data, stride); if (pix == NULL) { errno = ENOMEM; perror(__func__); abort(); } double dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; double scale = term->font_is_sized_by_dpi ? 1. : term->scale; double cell_size = sqrt(pow(term->cell_width, 2) + pow(term->cell_height, 2)); int base_thickness = (double)term->conf->tweak.box_drawing_base_thickness * scale * cell_size * dpi / 72.0; base_thickness = max(base_thickness, 1); int y_third_0 = 0, y_third_1 = 0; switch (height % 3) { case 0: y_third_0 = height / 3; y_third_1 = 2 * height / 3; break; case 1: y_third_0 = height / 3; y_third_1 = 2 * height / 3 + 1; break; case 2: y_third_0 = height / 3 + 1; y_third_1 = y_third_0 + height / 3; break; } /* TODO */ int y_quad_0 = 0, y_quad_1 = 0, y_quad_2 = 0; switch (height % 4) { case 0: y_quad_0 = height / 4; y_quad_1 = height / 2; y_quad_2 = 3 * height / 4; break; case 1: y_quad_0 = height / 4; y_quad_1 = height / 2; y_quad_2 = 3 * height / 4; break; case 2: y_quad_0 = height / 4; y_quad_1 = height / 2; y_quad_2 = 3 * height / 4; break; case 3: y_quad_0 = height / 4; y_quad_1 = height / 2; y_quad_2 = 3 * height / 4; break; } struct buf buf = { .data = data, .pix = pix, .format = fmt, .width = width, .height = height, .stride = stride, .solid_shades = term->conf->tweak.box_drawing_solid_shades, .thickness = { [LIGHT] = _thickness(base_thickness, LIGHT), [HEAVY] = _thickness(base_thickness, HEAVY), }, /* Overlap when width is odd */ .x_halfs = { round(width / 2.), /* Endpoint first half */ width / 2, /* Startpoint second half */ }, .y_thirds = { y_third_0, /* Endpoint first third, start point second third */ y_third_1, /* Endpoint second third, start point last third */ }, .y_quads = { y_quad_0, y_quad_1, y_quad_2, }, }; LOG_DBG("LIGHT=%d, HEAVY=%d", buf.thickness[LIGHT], buf.thickness[HEAVY]); draw_glyph(&buf, wc); struct fcft_glyph *glyph = xmalloc(sizeof(*glyph)); *glyph = (struct fcft_glyph){ .cp = wc, .cols = 1, .pix = buf.pix, .x = -term->font_x_ofs, .y = term->font_baseline, .width = width, .height = height, .advance = { .x = width, .y = height, }, }; return glyph; } foot-1.21.0/box-drawing.h000066400000000000000000000002241476600145200151370ustar00rootroot00000000000000#pragma once #include #include struct terminal; struct fcft_glyph *box_drawing(const struct terminal *term, char32_t wc); foot-1.21.0/char32.c000066400000000000000000000213451476600145200140020ustar00rootroot00000000000000#include "char32.h" #include #include #include #include #include #if defined __has_include #if __has_include () #include #endif #endif #define LOG_MODULE "char32" #define LOG_ENABLE_DBG 0 #include "log.h" #include "debug.h" #include "macros.h" #include "xmalloc.h" /* * For now, assume we can map directly to the corresponding wchar_t * functions. This is true if: * * - both data types have the same size * - both use the same encoding (though we require that encoding to be UTF-32) */ _Static_assert( sizeof(wchar_t) == sizeof(char32_t), "wchar_t vs. char32_t size mismatch"); #if !defined(__STDC_UTF_32__) || !__STDC_UTF_32__ #error "char32_t does not use UTF-32" #endif #if (!defined(__STDC_ISO_10646__) || !__STDC_ISO_10646__) && !defined(__FreeBSD__) #error "wchar_t does not use UTF-32" #endif UNITTEST { xassert(c32len(U"") == 0); xassert(c32len(U"foobar") == 6); } UNITTEST { xassert(c32cmp(U"foobar", U"foobar") == 0); xassert(c32cmp(U"foo", U"foobar") < 0); xassert(c32cmp(U"foobar", U"foo") > 0); xassert(c32cmp(U"a", U"b") < 0); xassert(c32cmp(U"b", U"a") > 0); } UNITTEST { char32_t copy[16]; char32_t *ret = c32ncpy(copy, U"foobar", 16); xassert(ret == copy); xassert(copy[0] == U'f'); xassert(copy[1] == U'o'); xassert(copy[2] == U'o'); xassert(copy[3] == U'b'); xassert(copy[4] == U'a'); xassert(copy[5] == U'r'); unsigned char zeroes[(16 - 6) * sizeof(copy[0])] = {0}; xassert(memcmp(©[6], zeroes, sizeof(zeroes)) == 0); } UNITTEST { char32_t copy[16]; memset(copy, 0x55, sizeof(copy)); char32_t *ret = c32cpy(copy, U"foobar"); xassert(ret == copy); xassert(copy[0] == U'f'); xassert(copy[1] == U'o'); xassert(copy[2] == U'o'); xassert(copy[3] == U'b'); xassert(copy[4] == U'a'); xassert(copy[5] == U'r'); xassert(copy[6] == U'\0'); unsigned char fives[(16 - 6 - 1) * sizeof(copy[0])]; memset(fives, 0x55, sizeof(fives)); xassert(memcmp(©[7], fives, sizeof(fives)) == 0); } UNITTEST { xassert(c32casecmp(U"foobar", U"FOOBAR") == 0); xassert(c32casecmp(U"foo", U"FOOO") < 0); xassert(c32casecmp(U"FOOO", U"foo") > 0); xassert(c32casecmp(U"a", U"B") < 0); xassert(c32casecmp(U"B", U"a") > 0); } UNITTEST { xassert(c32ncasecmp(U"foo", U"FOObar", 3) == 0); xassert(c32ncasecmp(U"foo", U"FOOO", 4) < 0); xassert(c32ncasecmp(U"FOOO", U"foo", 4) > 0); xassert(c32ncasecmp(U"a", U"BB", 1) < 0); xassert(c32ncasecmp(U"BB", U"a", 1) > 0); } UNITTEST { char32_t dst[32] = U"foobar"; char32_t *ret = c32ncat(dst, U"12345678XXXXXXXXX", 8); xassert(ret == dst); xassert(c32cmp(dst, U"foobar12345678") == 0); } UNITTEST { char32_t dst[32] = U"foobar"; char32_t *ret = c32cat(dst, U"12345678"); xassert(ret == dst); xassert(c32cmp(dst, U"foobar12345678") == 0); } UNITTEST { char32_t *c = xc32dup(U"foobar"); xassert(c32cmp(c, U"foobar") == 0); free(c); c = xc32dup(U""); xassert(c32cmp(c, U"") == 0); free(c); } size_t mbsntoc32(char32_t *dst, const char *src, size_t nms, size_t len) { mbstate_t ps = {0}; char32_t *out = dst; const char *in = src; size_t consumed = 0; size_t chars = 0; size_t rc; while ((out == NULL || chars < len) && consumed < nms && (rc = mbrtoc32(out, in, nms - consumed, &ps)) != 0) { switch (rc) { case 0: goto done; case (size_t)-1: case (size_t)-2: case (size_t)-3: goto err; } in += rc; consumed += rc; chars++; if (out != NULL) out++; } done: return chars; err: return (size_t)-1; } UNITTEST { const char input[] = "foobarzoo"; char32_t c32[32]; size_t ret = mbsntoc32(NULL, input, sizeof(input), 0); xassert(ret == 9); memset(c32, 0x55, sizeof(c32)); ret = mbsntoc32(c32, input, sizeof(input), 32); xassert(ret == 9); xassert(c32[0] == U'f'); xassert(c32[1] == U'o'); xassert(c32[2] == U'o'); xassert(c32[3] == U'b'); xassert(c32[4] == U'a'); xassert(c32[5] == U'r'); xassert(c32[6] == U'z'); xassert(c32[7] == U'o'); xassert(c32[8] == U'o'); xassert(c32[9] == U'\0'); xassert(c32[10] == 0x55555555); memset(c32, 0x55, sizeof(c32)); ret = mbsntoc32(c32, input, 1, 32); xassert(ret == 1); xassert(c32[0] == U'f'); xassert(c32[1] == 0x55555555); memset(c32, 0x55, sizeof(c32)); ret = mbsntoc32(c32, input, sizeof(input), 1); xassert(ret == 1); xassert(c32[0] == U'f'); xassert(c32[1] == 0x55555555); } UNITTEST { const char input[] = "foobarzoo"; char32_t c32[32]; size_t ret = mbstoc32(NULL, input, 0); xassert(ret == 9); memset(c32, 0x55, sizeof(c32)); ret = mbstoc32(c32, input, 32); xassert(ret == 9); xassert(c32[0] == U'f'); xassert(c32[1] == U'o'); xassert(c32[2] == U'o'); xassert(c32[3] == U'b'); xassert(c32[4] == U'a'); xassert(c32[5] == U'r'); xassert(c32[6] == U'z'); xassert(c32[7] == U'o'); xassert(c32[8] == U'o'); xassert(c32[9] == U'\0'); xassert(c32[10] == 0x55555555); memset(c32, 0x55, sizeof(c32)); ret = mbstoc32(c32, input, 1); xassert(ret == 1); xassert(c32[0] == U'f'); xassert(c32[1] == 0x55555555); } char32_t * ambstoc32(const char *src) { if (src == NULL) return NULL; const size_t src_len = strlen(src); char32_t *ret = xmalloc((src_len + 1) * sizeof(ret[0])); mbstate_t ps = {0}; char32_t *out = ret; const char *in = src; const char *const end = src + src_len + 1; size_t chars = 0; size_t rc; while ((rc = mbrtoc32(out, in, end - in, &ps)) != 0) { switch (rc) { case (size_t)-1: case (size_t)-2: case (size_t)-3: goto err; } in += rc; out++; chars++; } *out = U'\0'; ret = xrealloc(ret, (chars + 1) * sizeof(ret[0])); return ret; err: free(ret); return NULL; } UNITTEST { const char* locale = setlocale(LC_CTYPE, "en_US.UTF-8"); if (!locale) locale = setlocale(LC_CTYPE, "C.UTF-8"); if (!locale) return; char32_t *hello = ambstoc32(u8"hello"); xassert(hello != NULL); xassert(hello[0] == U'h'); xassert(hello[1] == U'e'); xassert(hello[2] == U'l'); xassert(hello[3] == U'l'); xassert(hello[4] == U'o'); xassert(hello[5] == U'\0'); free(hello); char32_t *swedish = ambstoc32(u8"åäö"); xassert(swedish != NULL); xassert(swedish[0] == U'å'); xassert(swedish[1] == U'ä'); xassert(swedish[2] == U'ö'); xassert(swedish[3] == U'\0'); free(swedish); char32_t *emoji = ambstoc32(u8"👨‍👩‍👧‍👦"); xassert(emoji != NULL); xassert(emoji[0] == U'👨'); xassert(emoji[1] == U'‍'); xassert(emoji[2] == U'👩'); xassert(emoji[3] == U'‍'); xassert(emoji[4] == U'👧'); xassert(emoji[5] == U'‍'); xassert(emoji[6] == U'👦'); xassert(emoji[7] == U'\0'); free(emoji); xassert(ambstoc32(NULL) == NULL); xassert(setlocale(LC_CTYPE, "C") != NULL); } char * ac32tombs(const char32_t *src) { if (src == NULL) return NULL; const size_t src_len = c32len(src); size_t allocated = src_len + 1; char *ret = xmalloc(allocated); mbstate_t ps = {0}; char *out = ret; const char32_t *const end = src + src_len + 1; size_t bytes = 0; char mb[MB_CUR_MAX]; for (const char32_t *in = src; in < end; in++) { size_t rc = c32rtomb(mb, *in, &ps); switch (rc) { case (size_t)-1: goto err; } if (bytes + rc > allocated) { allocated *= 2; ret = xrealloc(ret, allocated); out = &ret[bytes]; } for (size_t i = 0; i < rc; i++, out++) *out = mb[i]; bytes += rc; } xassert(ret[bytes - 1] == '\0'); ret = xrealloc(ret, bytes); return ret; err: free(ret); return NULL; } UNITTEST { const char* locale = setlocale(LC_CTYPE, "en_US.UTF-8"); if (!locale) locale = setlocale(LC_CTYPE, "C.UTF-8"); if (!locale) return; char *s = ac32tombs(U"foobar"); xassert(s != NULL); xassert(strcmp(s, "foobar") == 0); free(s); s = ac32tombs(U"åäö"); xassert(s != NULL); xassert(strcmp(s, u8"åäö") == 0); free(s); s = ac32tombs(U"👨‍👩‍👧‍👦"); xassert(s != NULL); xassert(strcmp(s, u8"👨‍👩‍👧‍👦") == 0); free(s); xassert(ac32tombs(NULL) == NULL); xassert(setlocale(LC_CTYPE, "C") != NULL); } foot-1.21.0/char32.h000066400000000000000000000052721476600145200140100ustar00rootroot00000000000000#pragma once #include #include #include #include #include #include #include #if defined(FOOT_GRAPHEME_CLUSTERING) #include #endif static inline size_t c32len(const char32_t *s) { return wcslen((const wchar_t *)s); } static inline int c32cmp(const char32_t *s1, const char32_t *s2) { return wcscmp((const wchar_t *)s1, (const wchar_t *)s2); } static inline char32_t *c32ncpy(char32_t *dst, const char32_t *src, size_t n) { return (char32_t *)wcsncpy((wchar_t *)dst, (const wchar_t *)src, n); } static inline char32_t *c32cpy(char32_t *dst, const char32_t *src) { return (char32_t *)wcscpy((wchar_t *)dst, (const wchar_t *)src); } static inline char32_t *c32ncat(char32_t *dst, const char32_t *src, size_t n) { return (char32_t *)wcsncat((wchar_t *)dst, (const wchar_t *)src, n); } static inline char32_t *c32cat(char32_t *dst, const char32_t *src) { return (char32_t *)wcscat((wchar_t *)dst, (const wchar_t *)src); } static inline char32_t *c32dup(const char32_t *s) { return (char32_t *)wcsdup((const wchar_t *)s); } static inline char32_t *c32chr(const char32_t *s, char32_t c) { return (char32_t *)wcschr((const wchar_t *)s, c); } static inline int c32casecmp(const char32_t *s1, const char32_t *s2) { return wcscasecmp((const wchar_t *)s1, (const wchar_t *)s2); } static inline int c32ncasecmp(const char32_t *s1, const char32_t *s2, size_t n) { return wcsncasecmp((const wchar_t *)s1, (const wchar_t *)s2, n); } static inline char32_t toc32lower(char32_t c) { return (char32_t)towlower((wint_t)c); } static inline char32_t toc32upper(char32_t c) { return (char32_t)towupper((wint_t)c); } static inline bool isc32space(char32_t c32) { return iswspace((wint_t)c32); } static inline bool isc32print(char32_t c32) { return iswprint((wint_t)c32); } static inline bool isc32graph(char32_t c32) { return iswgraph((wint_t)c32); } static inline int c32width(char32_t c) { #if defined(FOOT_GRAPHEME_CLUSTERING) return utf8proc_charwidth((utf8proc_int32_t)c); #else return wcwidth((wchar_t)c); #endif } static inline int c32swidth(const char32_t *s, size_t n) { #if defined(FOOT_GRAPHEME_CLUSTERING) int width = 0; for (size_t i = 0; i < n; i++) width += utf8proc_charwidth((utf8proc_int32_t)s[i]); return width; #else return wcswidth((const wchar_t *)s, n); #endif } size_t mbsntoc32(char32_t *dst, const char *src, size_t nms, size_t len); char32_t *ambstoc32(const char *src); char *ac32tombs(const char32_t *src); static inline size_t mbstoc32(char32_t *dst, const char *src, size_t len) { return mbsntoc32(dst, src, strlen(src) + 1, len); } foot-1.21.0/client-protocol.h000066400000000000000000000013331476600145200160350ustar00rootroot00000000000000#pragma once #include #include #include struct client_string { uint16_t len; /* char str[static len]; */ }; struct client_data { bool hold:1; bool no_wait:1; bool xdga_token:1; uint8_t reserved:5; uint8_t token_len; uint16_t cwd_len; uint16_t override_count; uint16_t argc; uint16_t env_count; /* char cwd[static cwd_len]; */ /* char token[static token_len]; */ /* struct client_string overrides[static override_count]; */ /* struct client_string argv[static argc]; */ /* struct client_string envp[static env_count]; */ } __attribute__((packed)); _Static_assert(sizeof(struct client_data) == 10, "protocol struct size error"); foot-1.21.0/client.c000066400000000000000000000410171476600145200141740ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "foot-client" #define LOG_ENABLE_DBG 0 #include "log.h" #include "client-protocol.h" #include "debug.h" #include "foot-features.h" #include "macros.h" #include "util.h" #include "xmalloc.h" extern char **environ; struct string { size_t len; char *str; }; typedef tll(struct string) string_list_t; static volatile sig_atomic_t aborted = 0; static void sig_handler(int signo) { aborted = 1; } static ssize_t sendall(int sock, const void *_buf, size_t len) { const uint8_t *buf = _buf; size_t left = len; while (left > 0) { ssize_t r = send(sock, buf, left, MSG_NOSIGNAL); if (r < 0) { if (errno == EINTR) continue; return r; } buf += r; left -= r; } return len; } static void print_usage(const char *prog_name) { static const char options[] = "\nOptions:\n" " -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n" " -T,--title=TITLE initial window title (foot)\n" " -a,--app-id=ID window application ID (foot)\n" " -w,--window-size-pixels=WIDTHxHEIGHT initial width and height, in pixels\n" " -W,--window-size-chars=WIDTHxHEIGHT initial width and height, in characters\n" " -m,--maximized start in maximized mode\n" " -F,--fullscreen start in fullscreen mode\n" " -L,--login-shell start shell as a login shell\n" " -D,--working-directory=DIR directory to start in (CWD)\n" " -s,--server-socket=PATH path to the server UNIX domain socket (default=$XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock)\n" " -H,--hold remain open after child process exits\n" " -N,--no-wait detach the client process from the running terminal, exiting immediately\n" " -o,--override=[section.]key=value override configuration option\n" " -E, --client-environment exec shell using footclient's environment, instead of the server's\n" " -d,--log-level={info|warning|error|none} log level (warning)\n" " -l,--log-colorize=[{never|always|auto}] enable/disable colorization of log output on stderr\n" " -v,--version show the version number and quit\n" " -e ignored (for compatibility with xterm -e)\n"; printf("Usage: %s [OPTIONS...]\n", prog_name); printf("Usage: %s [OPTIONS...] command [ARGS...]\n", prog_name); puts(options); } static bool NOINLINE push_string(string_list_t *string_list, const char *s, uint64_t *total_len) { size_t len = strlen(s) + 1; if (len >= 1 << (8 * sizeof(uint16_t))) { LOG_ERR("string length overflow"); return false; } struct string o = {len, xstrdup(s)}; tll_push_back(*string_list, o); *total_len += sizeof(struct client_string) + o.len; return true; } static void free_string_list(string_list_t *string_list) { tll_foreach(*string_list, it) { free(it->item.str); tll_remove(*string_list, it); } } static bool send_string_list(int fd, const string_list_t *string_list) { tll_foreach(*string_list, it) { const struct client_string s = {it->item.len}; if (sendall(fd, &s, sizeof(s)) < 0 || sendall(fd, it->item.str, s.len) < 0) { LOG_ERRNO("failed to send setup packet to server"); return false; } } return true; } int main(int argc, char *const *argv) { /* Custom exit code, to enable users to differentiate between foot * itself failing, and the client application failing */ static const int foot_exit_failure = -36; int ret = foot_exit_failure; const char *const prog_name = argc > 0 ? argv[0] : ""; static const struct option longopts[] = { {"term", required_argument, NULL, 't'}, {"title", required_argument, NULL, 'T'}, {"app-id", required_argument, NULL, 'a'}, {"window-size-pixels", required_argument, NULL, 'w'}, {"window-size-chars", required_argument, NULL, 'W'}, {"maximized", no_argument, NULL, 'm'}, {"fullscreen", no_argument, NULL, 'F'}, {"login-shell", no_argument, NULL, 'L'}, {"working-directory", required_argument, NULL, 'D'}, {"server-socket", required_argument, NULL, 's'}, {"hold", no_argument, NULL, 'H'}, {"no-wait", no_argument, NULL, 'N'}, {"override", required_argument, NULL, 'o'}, {"client-environment", no_argument, NULL, 'E'}, {"log-level", required_argument, NULL, 'd'}, {"log-colorize", optional_argument, NULL, 'l'}, {"version", no_argument, NULL, 'v'}, {"help", no_argument, NULL, 'h'}, {NULL, no_argument, NULL, 0}, }; const char *custom_cwd = NULL; const char *server_socket_path = NULL; enum log_class log_level = LOG_CLASS_WARNING; enum log_colorize log_colorize = LOG_COLORIZE_AUTO; bool hold = false; bool client_environment = false; /* Used to format overrides */ bool no_wait = false; /* For XDG activation */ const char *token = getenv("XDG_ACTIVATION_TOKEN"); bool xdga_token = token != NULL; size_t token_len = xdga_token ? strlen(token) + 1 : 0; char buf[1024]; /* Total packet length, not (yet) including overrides or argv[] */ uint64_t total_len = 0; /* malloc:ed and needs to be in scope of all goto's */ int fd = -1; char *_cwd = NULL; struct client_string *cargv = NULL; string_list_t overrides = tll_init(); string_list_t envp = tll_init(); while (true) { int c = getopt_long(argc, argv, "+t:T:a:w:W:mFLD:s:HNo:Ed:l::veh", longopts, NULL); if (c == -1) break; switch (c) { case 't': snprintf(buf, sizeof(buf), "term=%s", optarg); if (!push_string(&overrides, buf, &total_len)) goto err; break; case 'T': snprintf(buf, sizeof(buf), "title=%s", optarg); if (!push_string(&overrides, buf, &total_len)) goto err; break; case 'a': snprintf(buf, sizeof(buf), "app-id=%s", optarg); if (!push_string(&overrides, buf, &total_len)) goto err; break; case 'L': if (!push_string(&overrides, "login-shell=yes", &total_len)) goto err; break; case 'D': { struct stat st; if (stat(optarg, &st) < 0 || !(st.st_mode & S_IFDIR)) { fprintf(stderr, "error: %s: not a directory\n", optarg); goto err; } custom_cwd = optarg; break; } case 'w': { unsigned width, height; if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) { fprintf(stderr, "error: invalid window-size-pixels: %s\n", optarg); goto err; } snprintf(buf, sizeof(buf), "initial-window-size-pixels=%ux%u", width, height); if (!push_string(&overrides, buf, &total_len)) goto err; break; } case 'W': { unsigned width, height; if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) { fprintf(stderr, "error: invalid window-size-chars: %s\n", optarg); goto err; } snprintf(buf, sizeof(buf), "initial-window-size-chars=%ux%u", width, height); if (!push_string(&overrides, buf, &total_len)) goto err; break; } case 'm': if (!push_string(&overrides, "initial-window-mode=maximized", &total_len)) goto err; break; case 'F': if (!push_string(&overrides, "initial-window-mode=fullscreen", &total_len)) goto err; break; case 's': server_socket_path = optarg; break; case 'H': hold = true; break; case 'N': no_wait = true; break; case 'o': if (!push_string(&overrides, optarg, &total_len)) goto err; break; case 'E': client_environment = true; break; case 'd': { int lvl = log_level_from_string(optarg); if (unlikely(lvl < 0)) { fprintf( stderr, "-d,--log-level: %s: argument must be one of %s\n", optarg, log_level_string_hint()); goto err; } log_level = lvl; break; } case 'l': if (optarg == NULL || streq(optarg, "auto")) log_colorize = LOG_COLORIZE_AUTO; else if (streq(optarg, "never")) log_colorize = LOG_COLORIZE_NEVER; else if (streq(optarg, "always")) log_colorize = LOG_COLORIZE_ALWAYS; else { fprintf(stderr, "%s: argument must be one of 'never', 'always' or 'auto'\n", optarg); goto err; } break; case 'v': print_version_and_features("footclient "); ret = EXIT_SUCCESS; goto err; case 'h': print_usage(prog_name); ret = EXIT_SUCCESS; goto err; case 'e': break; case '?': goto err; } } if (argc > 0) { argc -= optind; argv += optind; } log_init(log_colorize, false, LOG_FACILITY_USER, log_level); fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd == -1) { LOG_ERRNO("failed to create socket"); goto err; } struct sockaddr_un addr = {.sun_family = AF_UNIX}; if (server_socket_path != NULL) { strncpy(addr.sun_path, server_socket_path, sizeof(addr.sun_path) - 1); if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { LOG_ERR("%s: failed to connect (is 'foot --server' running?)", server_socket_path); goto err; } } else { bool connected = false; const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); if (xdg_runtime != NULL) { const char *wayland_display = getenv("WAYLAND_DISPLAY"); if (wayland_display != NULL) { snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/foot-%s.sock", xdg_runtime, wayland_display); connected = (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) == 0); } if (!connected) { LOG_WARN("%s: failed to connect, will now try %s/foot.sock", addr.sun_path, xdg_runtime); snprintf(addr.sun_path, sizeof(addr.sun_path), "%s/foot.sock", xdg_runtime); connected = (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) == 0); } if (!connected) LOG_WARN("%s: failed to connect, will now try /tmp/foot.sock", addr.sun_path); } if (!connected) { strncpy(addr.sun_path, "/tmp/foot.sock", sizeof(addr.sun_path) - 1); if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { LOG_ERRNO("failed to connect (is 'foot --server' running?)"); goto err; } } } const char *cwd = custom_cwd; if (cwd == NULL) { size_t buf_len = 1024; do { _cwd = xrealloc(_cwd, buf_len); errno = 0; if (getcwd(_cwd, buf_len) == NULL && errno != ERANGE) { LOG_ERRNO("failed to get current working directory"); goto err; } buf_len *= 2; } while (errno == ERANGE); cwd = _cwd; } const char *pwd = getenv("PWD"); if (pwd != NULL) { char *resolved_path_cwd = realpath(cwd, NULL); char *resolved_path_pwd = realpath(pwd, NULL); if (resolved_path_cwd != NULL && resolved_path_pwd != NULL && streq(resolved_path_cwd, resolved_path_pwd)) { /* * The resolved path of $PWD matches the resolved path of * the *actual* working directory - use $PWD. * * This makes a difference when $PWD refers to a symlink. */ cwd = pwd; } free(resolved_path_cwd); free(resolved_path_pwd); } if (client_environment) { for (char **e = environ; *e != NULL; e++) { if (!push_string(&envp, *e, &total_len)) goto err; } } /* String lengths, including NULL terminator */ const size_t cwd_len = strlen(cwd) + 1; const size_t override_count = tll_length(overrides); const size_t env_count = tll_length(envp); const struct client_data data = { .hold = hold, .no_wait = no_wait, .xdga_token = xdga_token, .token_len = token_len, .cwd_len = cwd_len, .override_count = override_count, .argc = argc, .env_count = env_count, }; /* Total packet length, not (yet) including argv[] */ total_len += sizeof(data) + cwd_len + token_len; /* Add argv[] size to total packet length */ cargv = xmalloc(argc * sizeof(cargv[0])); for (size_t i = 0; i < argc; i++) { const size_t arg_len = strlen(argv[i]) + 1; if (arg_len >= 1 << (8 * sizeof(cargv[i].len))) { LOG_ERR("argv length overflow"); goto err; } cargv[i].len = arg_len; total_len += sizeof(cargv[i]) + cargv[i].len; } /* Check for size overflows */ if (total_len >= 1llu << (8 * sizeof(uint32_t)) || cwd_len >= 1 << (8 * sizeof(data.cwd_len)) || token_len >= 1 << (8 * sizeof(data.token_len)) || override_count > (size_t)(unsigned int)data.override_count || argc > (int)(unsigned int)data.argc || env_count > (size_t)(unsigned int)data.env_count) { LOG_ERR("size overflow"); goto err; } /* Send everything except argv[] */ if (sendall(fd, &(uint32_t){total_len}, sizeof(uint32_t)) < 0 || sendall(fd, &data, sizeof(data)) < 0 || sendall(fd, cwd, cwd_len) < 0) { LOG_ERRNO("failed to send setup packet to server"); goto err; } /* Send XDGA token, if we have one */ if (xdga_token) { if (sendall(fd, token, token_len) != token_len) { LOG_ERRNO("failed to send xdg activation token to server"); goto err; } } /* Send overrides */ if (!send_string_list(fd, &overrides)) goto err; /* Send argv[] */ for (size_t i = 0; i < argc; i++) { if (sendall(fd, &cargv[i], sizeof(cargv[i])) < 0 || sendall(fd, argv[i], cargv[i].len) < 0) { LOG_ERRNO("failed to send setup packet (argv) to server"); goto err; } } /* Send environment */ if (!send_string_list(fd, &envp)) goto err; struct sigaction sa = {.sa_handler = &sig_handler}; sigemptyset(&sa.sa_mask); if (sigaction(SIGINT, &sa, NULL) < 0 || sigaction(SIGTERM, &sa, 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); if (rcvd == -1 && errno == EINTR) xassert(aborted); else if (rcvd != sizeof(exit_code)) LOG_ERRNO("failed to read server response"); else ret = exit_code; err: free_string_list(&envp); free_string_list(&overrides); free(cargv); free(_cwd); if (fd != -1) close(fd); log_deinit(); return ret; } foot-1.21.0/commands.c000066400000000000000000000061201476600145200145130ustar00rootroot00000000000000#include "commands.h" #define LOG_MODULE "commands" #define LOG_ENABLE_DBG 0 #include "log.h" #include "grid.h" #include "render.h" #include "selection.h" #include "terminal.h" #include "url-mode.h" #include "util.h" void cmd_scrollback_up(struct terminal *term, int rows) { if (term->grid == &term->alt) return; if (urls_mode_is_active(term)) return; const struct grid *grid = term->grid; const int view = grid->view; const int grid_rows = grid->num_rows; /* The view row number in scrollback relative coordinates. This is * the maximum number of rows we're allowed to scroll */ int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); int view_sb_rel = grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, view); rows = min(rows, view_sb_rel); if (rows == 0) return; int new_view = (view + grid_rows) - rows; new_view &= grid_rows - 1; xassert(new_view != view); xassert(grid->rows[new_view] != NULL); #if defined(_DEBUG) for (int r = 0; r < term->rows; r++) xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL); #endif LOG_DBG("scrollback UP: %d -> %d (offset = %d, rows = %d)", view, new_view, offset, grid_rows); selection_view_up(term, new_view); term->grid->view = new_view; if (rows < term->rows) { term_damage_scroll( term, DAMAGE_SCROLL_REVERSE_IN_VIEW, (struct scroll_region){0, term->rows}, rows); term_damage_rows_in_view(term, 0, rows - 1); } else term_damage_view(term); render_refresh_urls(term); render_refresh(term); } void cmd_scrollback_down(struct terminal *term, int rows) { if (term->grid == &term->alt) return; if (urls_mode_is_active(term)) return; const struct grid *grid = term->grid; const int offset = grid->offset; const int view = grid->view; const int grid_rows = grid->num_rows; const int screen_rows = term->rows; const int scrollback_end = offset; /* Number of rows to scroll, without going past the scrollback end */ int max_rows = 0; if (view <= scrollback_end) max_rows = scrollback_end - view; else max_rows = offset + (grid_rows - view); rows = min(rows, max_rows); if (rows == 0) return; int new_view = (view + rows) & (grid_rows - 1); xassert(new_view != view); xassert(grid->rows[new_view] != NULL); #if defined(_DEBUG) for (int r = 0; r < term->rows; r++) xassert(grid->rows[(new_view + r) & (grid_rows - 1)] != NULL); #endif LOG_DBG("scrollback DOWN: %d -> %d (offset = %d, rows = %d)", view, new_view, offset, grid_rows); selection_view_down(term, new_view); term->grid->view = new_view; if (rows < term->rows) { term_damage_scroll( term, DAMAGE_SCROLL_IN_VIEW, (struct scroll_region){0, term->rows}, rows); term_damage_rows_in_view(term, term->rows - rows, screen_rows - 1); } else term_damage_view(term); render_refresh_urls(term); render_refresh(term); } foot-1.21.0/commands.h000066400000000000000000000002311476600145200145150ustar00rootroot00000000000000#pragma once #include "terminal.h" void cmd_scrollback_up(struct terminal *term, int rows); void cmd_scrollback_down(struct terminal *term, int rows); foot-1.21.0/completions/000077500000000000000000000000001476600145200151035ustar00rootroot00000000000000foot-1.21.0/completions/bash/000077500000000000000000000000001476600145200160205ustar00rootroot00000000000000foot-1.21.0/completions/bash/foot000066400000000000000000000052621476600145200167170ustar00rootroot00000000000000# Bash completion script for foot _foot() { COMPREPLY=() local cur prev flags word commands match previous_words i offset flags=( "--app-id" "--check-config" "--config" "--font" "--fullscreen" "--help" "--hold" "--log-colorize" "--log-level" "--log-no-syslog" "--login-shell" "--maximized" "--override" "--print-pid" "--pty" "--server" "--term" "--title" "--version" "--window-size-pixels" "--window-size-chars" "--working-directory" ) flags="${flags[@]}" cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} # Check if positional argument is completed previous_words=( "${COMP_WORDS[@]}" ) unset previous_words[-1] commands=$(compgen -c | grep -vFx "$(compgen -k)" | grep -vE '^([.:[]|foot)$' | sort -u) i=0 for word in "${previous_words[@]}" ; do match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null) if [[ ! -z "$match" ]] ; then if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--config|--font|--log-level|--pty|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then (( i++ )) continue fi # Positional argument found offset=$i fi (( i++ )) done if [[ ! -z "$offset" ]] ; then # Depends on bash_completion being available declare -F _command_offset >/dev/null || return 1 _command_offset $offset return 0 elif [[ ${cur} == --* ]] ; then COMPREPLY=( $(compgen -W "${flags}" -- ${cur}) ) return 0 fi case "$prev" in --config|--print-pid|--server|-[cps]) compopt -o default ;; --working-directory|-D) compopt -o dirnames ;; --term|-t) command -v toe > /dev/null || return 1 COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 !~ /[+]/ {print $1}')" -- ${cur}) ) ;; --font|-f) command -v fc-list > /dev/null || return 1 COMPREPLY=( $(compgen -W "$(fc-list : family | sed 's/,/\n/g' | uniq | tr -d ' ')" -- ${cur}) ) ;; --log-level|-d) COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; --log-colorize|-l) COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; --app-id|--help|--override|--pty|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC]) # Don't autocomplete for these flags : ;; *) # Complete commands from $PATH COMPREPLY=( $(compgen -c -- ${cur}) ) ;; esac return 0 } complete -F _foot foot foot-1.21.0/completions/bash/footclient000066400000000000000000000046461476600145200201230ustar00rootroot00000000000000# Bash completion script for footclient _footclient() { COMPREPLY=() local cur prev flags word commands match previous_words i offset flags=( "--app-id" "--fullscreen" "--help" "--hold" "--login-shell" "--log-level" "--log-colorize" "--maximized" "--override" "--client-environment" "--server-socket" "--term" "--title" "--version" "--window-size-pixels" "--window-size-chars" "--working-directory" ) flags="${flags[@]}" cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} # Check if positional argument is completed previous_words=( "${COMP_WORDS[@]}" ) unset previous_words[-1] commands=$(compgen -c | grep -vFx "$(compgen -k)" | grep -vE '^([.:[]|footclient)$' | sort -u) i=0 for word in "${previous_words[@]}" ; do match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null) if [[ ! -z "$match" ]] ; then if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--log-level|--server-socket|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then (( i++ )) continue fi # Positional argument found offset=$i fi (( i++ )) done if [[ ! -z "$offset" ]] ; then # Depends on bash_completion being available declare -F _command_offset >/dev/null || return 1 _command_offset $offset return 0 elif [[ ${cur} == --* ]] ; then COMPREPLY=( $(compgen -W "${flags}" -- ${cur}) ) return 0 fi case "$prev" in --server-socket|-s) compopt -o default ;; --working-directory|-D) compopt -o dirnames ;; --term|-t) command -v toe > /dev/null || return 1 COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 ~ /[+]/ {next}; {print $1}')" -- ${cur}) ) ;; --log-level|-d) COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; --log-colorize|-l) COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; --app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|-[ahoTvWw]) # Don't autocomplete for these flags : ;; *) # Complete commands from $PATH COMPREPLY=( $(compgen -c -- ${cur}) ) ;; esac return 0 } complete -F _footclient footclient foot-1.21.0/completions/fish/000077500000000000000000000000001476600145200160345ustar00rootroot00000000000000foot-1.21.0/completions/fish/foot.fish000066400000000000000000000065321476600145200176640ustar00rootroot00000000000000complete -c foot -x -a "(__fish_complete_subcommand)" complete -c foot -r -s c -l config -d "path to configuration file (XDG_CONFIG_HOME/foot/foot.ini)" complete -c foot -s C -l check-config -d "verify configuration and exit with 0 if ok, otherwise exit with 1" complete -c foot -x -s o -l override -d "configuration option to override, in form SECTION.KEY=VALUE" complete -c foot -x -s f -l font -a "(fc-list : family | sed 's/,/\n/g' | sort | uniq)" -d "font name and style in fontconfig format (monospace)" complete -c foot -x -s t -l term -a '(find /usr/share/terminfo -type f -printf "%f\n")' -d "value to set the environment variable TERM to (foot)" complete -c foot -x -s T -l title -d "initial window title" complete -c foot -x -s a -l app-id -d "value to set the app-id property on the Wayland window to (foot)" complete -c foot -s m -l maximized -d "start in maximized mode" complete -c foot -s F -l fullscreen -d "start in fullscreen mode" complete -c foot -s L -l login-shell -d "start shell as a login shell" complete -c foot -F -s D -l working-directory -d "initial working directory for the client application (CWD)" complete -c foot -x -s w -l window-size-pixels -d "window WIDTHxHEIGHT, in pixels (700x500)" complete -c foot -x -s W -l window-size-chars -d "window WIDTHxHEIGHT, in characters (not set)" complete -c foot -F -s s -l server -d "run as server; open terminals by running footclient" complete -c foot -s H -l hold -d "remain open after child process exits" complete -c foot -r -s p -l print-pid -d "print PID to this file or FD when up and running (server mode only)" complete -c foot -x -s d -l log-level -a "info warning error none" -d "log-level (warning)" complete -c foot -x -s l -l log-colorize -a "always never auto" -d "enable or disable colorization of log output on stderr" complete -c foot -s S -l log-no-syslog -d "disable syslog logging (server mode only)" complete -c foot -r -l pty -d "display an existing pty instead of creating one" complete -c foot -s v -l version -d "show the version number and quit" complete -c foot -s h -l help -d "show help message and quit" foot-1.21.0/completions/fish/footclient.fish000066400000000000000000000056151476600145200210640ustar00rootroot00000000000000complete -c footclient -x -a "(__fish_complete_subcommand)" complete -c footclient -x -s t -l term -a '(find /usr/share/terminfo -type f -printf "%f\n")' -d "value to set the environment variable TERM to (foot)" complete -c footclient -x -s T -l title -d "initial window title" complete -c footclient -x -s a -l app-id -d "value to set the app-id property on the Wayland window to (foot)" complete -c footclient -s m -l maximized -d "start in maximized mode" complete -c footclient -s F -l fullscreen -d "start in fullscreen mode" complete -c footclient -s L -l login-shell -d "start shell as a login shell" complete -c footclient -F -s D -l working-directory -d "initial working directory for the client application (CWD)" complete -c footclient -x -s w -l window-size-pixels -d "window WIDTHxHEIGHT, in pixels (700x500)" complete -c footclient -x -s W -l window-size-chars -d "window WIDTHxHEIGHT, in characters (not set)" complete -c footclient -F -s s -l server-socket -d "override the default path to the foot server socket ($XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock)" complete -c footclient -s H -l hold -d "remain open after child process exits" complete -c footclient -s N -l no-wait -d "detach the client process from the running terminal, exiting immediately" complete -c footclient -x -s o -l override -d "configuration option to override, in form SECTION.KEY=VALUE" complete -c footclient -s E -l client-environment -d "child process inherits footclient's environment, instead of the server's" complete -c footclient -x -s d -l log-level -a "info warning error none" -d "log-level (info)" complete -c footclient -x -s l -l log-colorize -a "always never auto" -d "enable or disable colorization of log output on stderr" complete -c footclient -s v -l version -d "show the version number and quit" complete -c footclient -s h -l help -d "show help message and quit" foot-1.21.0/completions/meson.build000066400000000000000000000011511476600145200172430ustar00rootroot00000000000000zsh_install_dir = join_paths(get_option('datadir'), 'zsh', 'site-functions') fish_install_dir = join_paths(get_option('datadir'), 'fish', 'vendor_completions.d') bash_install_dir = join_paths(get_option('datadir'), 'bash-completion', 'completions') install_data('zsh/_foot', install_dir: zsh_install_dir) install_data('zsh/_footclient', install_dir: zsh_install_dir) install_data('fish/foot.fish', install_dir: fish_install_dir) install_data('fish/footclient.fish', install_dir: fish_install_dir) install_data('bash/foot', install_dir: bash_install_dir) install_data('bash/footclient', install_dir: bash_install_dir) foot-1.21.0/completions/zsh/000077500000000000000000000000001476600145200157075ustar00rootroot00000000000000foot-1.21.0/completions/zsh/_foot000066400000000000000000000047501476600145200167460ustar00rootroot00000000000000#compdef foot _arguments \ -s -S -C \ '(-c --config)'{-c,--config}'[path to configuration file (XDG_CONFIG_HOME/foot/foot.ini)]:config:_files' \ '(-C --check-config)'{-C,--check-config}'[verify configuration and exit with 0 if ok, otherwise exit with 1]' \ '(-o --override)'{-o,--override}'[configuration option to override, in form SECTION.KEY=VALUE]:()' \ '(-f --font)'{-f,--font}'[font name and style in fontconfig format (monospace)]:font:->fonts' \ '(-t --term)'{-t,--term}'[value to set the environment variable TERM to (foot)]:term:->terms' \ '(-T --title)'{-T,--title}'[initial window title]:()' \ '(-a --app-id)'{-a,--app-id}'[value to set the app-id property on the Wayland window to (foot)]:()' \ '(-m --maximized)'{-m,--maximized}'[start in maximized mode]' \ '(-F --fullscreen)'{-F,--fullscreen}'[start in fullscreen mode]' \ '(-L --login-shell)'{-L,--login-shell}'[start shell as a login shell]' \ '(-D --working-directory)'{-D,--working-directory}'[initial working directory for the client application (CWD)]:working_directory:_files' \ '(-w --window-size-pixels)'{-w,--window-size-pixels}'[window WIDTHxHEIGHT, in pixels (700x500)]:size_pixels:()' \ '(-W --window-size-chars)'{-W,--window-size-chars}'[window WIDTHxHEIGHT, in characters (not set)]:size_chars:()' \ '(-s --server)'{-s,--server}'[run as server; open terminals by running footclient]:server:_files' \ '(-H --hold)'{-H,--hold}'[remain open after child process exits]' \ '(-p --print-pid)'{-p,--print-pid}'[print PID to this file or FD when up and running (server mode only)]:pidfile:_files' \ '--pty=[display an existing pty instead of creating one]:pty:_files' \ '(-d --log-level)'{-d,--log-level}'[log level (warning)]:loglevel:(info warning error none)' \ '(-l --log-colorize)'{-l,--log-colorize}'[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \ '(-S --log-no-syslog)'{-s,--log-no-syslog}'[disable syslog logging (server mode only)]' \ '(-v --version)'{-v,--version}'[show the version number and quit]' \ '(-h --help)'{-h,--help}'[show help message and quit]' \ ':command: _command_names -e' \ '*::command arguments: _dispatch ${words[1]} ${words[1]}' case ${state} in fonts) IFS=$'\n' _values -s , 'font families' $(fc-list : family | sed 's/,/\n/g' | sort | uniq) unset IFS ;; terms) _values 'terminal definitions' /usr/share/terminfo/**/*(.:t) ;; esac foot-1.21.0/completions/zsh/_footclient000066400000000000000000000040401476600145200201350ustar00rootroot00000000000000#compdef footclient _arguments \ -s -S -C \ '(-t --term)'{-t,--term}'[value to set the environment variable TERM to (foot)]:term:->terms' \ '(-T --title)'{-T,--title}'[initial window title]:()' \ '(-a --app-id)'{-a,--app-id}'[value to set the app-id property on the Wayland window to (foot)]:()' \ '(-m --maximized)'{-m,--maximized}'[start in maximized mode]' \ '(-F --fullscreen)'{-F,--fullscreen}'[start in fullscreen mode]' \ '(-L --login-shell)'{-L,--login-shell}'[start shell as a login shell]' \ '(-D --working-directory)'{-D,--working-directory}'[initial working directory for the client application (CWD)]:working_directory:_files' \ '(-w --window-size-pixels)'{-w,--window-size-pixels}'[window WIDTHxHEIGHT, in pixels (700x500)]:size_pixels:()' \ '(-W --window-size-chars)'{-W,--window-size-chars}'[window WIDTHxHEIGHT, in characters (not set)]:size_chars:()' \ '(-s --server-socket)'{-s,--server-socket}'[override the default path to the foot server socket ($XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock)]:server:_files' \ '(-H --hold)'{-H,--hold}'[remain open after child process exits]' \ '(-N --no-wait)'{-N,--no-wait}'[detach the client process from the running terminal, exiting immediately]' \ '(-o --override)'{-o,--override}'[configuration option to override, in form SECTION.KEY=VALUE]:()' \ '(-E --client-environment)'{-E,--client-environment}"[child process inherits footclient's environment, instead of the server's]" \ '(-d --log-level)'{-d,--log-level}'[log level (warning)]:loglevel:(info warning error none)' \ '(-l --log-colorize)'{-l,--log-colorize}'[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \ '(-v --version)'{-v,--version}'[show the version number and quit]' \ '(-h --help)'{-h,--help}'[show help message and quit]' \ ':command: _command_names -e' \ '*::command arguments: _dispatch ${words[1]} ${words[1]}' case ${state} in terms) _values 'terminal definitions' /usr/share/terminfo/**/*(.:t) ;; esac foot-1.21.0/composed.c000066400000000000000000000063611476600145200145320ustar00rootroot00000000000000#include "composed.h" #include #include #include "debug.h" #include "terminal.h" uint32_t composed_key_from_chars(const uint32_t chars[], size_t count) { if (count == 0) return 0; uint32_t key = chars[0]; for (size_t i = 1; i < count; i++) key = composed_key_from_key(key, chars[i]); return key; } uint32_t composed_key_from_key(uint32_t prev_key, uint32_t next_char) { unsigned bits = 32 - __builtin_clz(CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO); /* Rotate old key 8 bits */ uint32_t new_key = (prev_key << 8) | (prev_key >> (bits - 8)); /* xor with new char */ new_key ^= next_char; /* Multiply with magic hash constant */ new_key *= 2654435761ul; /* And mask, to ensure the new value is within range */ new_key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; return new_key; } UNITTEST { const char32_t chars[] = U"abcdef"; uint32_t k1 = composed_key_from_key(chars[0], chars[1]); uint32_t k2 = composed_key_from_chars(chars, 2); xassert(k1 == k2); uint32_t k3 = composed_key_from_key(k2, chars[2]); uint32_t k4 = composed_key_from_chars(chars, 3); xassert(k3 == k4); } const struct composed * composed_lookup(struct composed *root, uint32_t key) { struct composed *node = root; while (node != NULL) { if (key == node->key) return node; node = key < node->key ? node->left : node->right; } return NULL; } const struct composed * composed_lookup_without_collision(struct composed *root, uint32_t *key, const char32_t *prefix_text, size_t prefix_len, char32_t wc, int forced_width) { while (true) { const struct composed *cc = composed_lookup(root, *key); if (cc == NULL) return NULL; bool match = cc->count == prefix_len + 1 && cc->forced_width == forced_width && cc->chars[prefix_len] == wc; if (match) { for (size_t i = 0; i < prefix_len; i++) { if (cc->chars[i] != prefix_text[i]) { match = false; break; } } } if (match) return cc; (*key)++; *key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; /* TODO: this will loop infinitely if the composed table is full */ } return NULL; } void composed_insert(struct composed **root, struct composed *node) { node->left = node->right = NULL; if (*root == NULL) { *root = node; return; } uint32_t key = node->key; struct composed *prev = NULL; struct composed *n = *root; while (n != NULL) { xassert(n->key != node->key); prev = n; n = key < n->key ? n->left : n->right; } xassert(prev != NULL); xassert(n == NULL); if (key < prev->key) { xassert(prev->left == NULL); prev->left = node; } else { xassert(prev->right == NULL); prev->right = node; } } void composed_free(struct composed *root) { if (root == NULL) return; composed_free(root->left); composed_free(root->right); free(root->chars); free(root); } foot-1.21.0/composed.h000066400000000000000000000013531476600145200145330ustar00rootroot00000000000000#pragma once #include #include struct composed { char32_t *chars; struct composed *left; struct composed *right; uint32_t key; uint8_t count; uint8_t width; uint8_t forced_width; }; uint32_t composed_key_from_chars(const uint32_t chars[], size_t count); uint32_t composed_key_from_key(uint32_t prev_key, uint32_t next_char); const struct composed *composed_lookup(struct composed *root, uint32_t key); const struct composed *composed_lookup_without_collision( struct composed *root, uint32_t *key, const char32_t *prefix, size_t prefix_len, char32_t wc, int forced_width); void composed_insert(struct composed **root, struct composed *node); void composed_free(struct composed *root); foot-1.21.0/config.c000066400000000000000000003616011476600145200141670ustar00rootroot00000000000000#include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "config" #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" #include "debug.h" #include "input.h" #include "key-binding.h" #include "macros.h" #include "tokenize.h" #include "util.h" #include "xmalloc.h" #include "xsnprintf.h" static const uint32_t default_foreground = 0xffffff; static const uint32_t default_background = 0x242424; static const size_t min_csd_border_width = 5; #define cube6(r, g) \ r|g|0x00, r|g|0x5f, r|g|0x87, r|g|0xaf, r|g|0xd7, r|g|0xff #define cube36(r) \ cube6(r, 0x0000), \ cube6(r, 0x5f00), \ cube6(r, 0x8700), \ cube6(r, 0xaf00), \ cube6(r, 0xd700), \ cube6(r, 0xff00) static const uint32_t default_color_table[256] = { // Regular 0x242424, 0xf62b5a, 0x47b413, 0xe3c401, 0x24acd4, 0xf2affd, 0x13c299, 0xe6e6e6, // Bright 0x616161, 0xff4d51, 0x35d450, 0xe9e836, 0x5dc5f8, 0xfeabf2, 0x24dfc4, 0xffffff, // 6x6x6 RGB cube // (color channels = i ? i*40+55 : 0, where i = 0..5) cube36(0x000000), cube36(0x5f0000), cube36(0x870000), cube36(0xaf0000), cube36(0xd70000), cube36(0xff0000), // 24 shades of gray // (color channels = i*10+8, where i = 0..23) 0x080808, 0x121212, 0x1c1c1c, 0x262626, 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e, 0x585858, 0x626262, 0x6c6c6c, 0x767676, 0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e, 0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee }; /* VT330/VT340 Programmer Reference Manual - Table 2-3 VT340 Default Color Map */ static const uint32_t default_sixel_colors[16] = { 0xff000000, 0xff3333cc, 0xffcc2121, 0xff33cc33, 0xffcc33cc, 0xff33cccc, 0xffcccc33, 0xff878787, 0xff424242, 0xff545499, 0xff994242, 0xff549954, 0xff995499, 0xff549999, 0xff999954, 0xffcccccc, }; static const char *const binding_action_map[] = { [BIND_ACTION_NONE] = NULL, [BIND_ACTION_NOOP] = "noop", [BIND_ACTION_SCROLLBACK_UP_PAGE] = "scrollback-up-page", [BIND_ACTION_SCROLLBACK_UP_HALF_PAGE] = "scrollback-up-half-page", [BIND_ACTION_SCROLLBACK_UP_LINE] = "scrollback-up-line", [BIND_ACTION_SCROLLBACK_DOWN_PAGE] = "scrollback-down-page", [BIND_ACTION_SCROLLBACK_DOWN_HALF_PAGE] = "scrollback-down-half-page", [BIND_ACTION_SCROLLBACK_DOWN_LINE] = "scrollback-down-line", [BIND_ACTION_SCROLLBACK_HOME] = "scrollback-home", [BIND_ACTION_SCROLLBACK_END] = "scrollback-end", [BIND_ACTION_CLIPBOARD_COPY] = "clipboard-copy", [BIND_ACTION_CLIPBOARD_PASTE] = "clipboard-paste", [BIND_ACTION_PRIMARY_PASTE] = "primary-paste", [BIND_ACTION_SEARCH_START] = "search-start", [BIND_ACTION_FONT_SIZE_UP] = "font-increase", [BIND_ACTION_FONT_SIZE_DOWN] = "font-decrease", [BIND_ACTION_FONT_SIZE_RESET] = "font-reset", [BIND_ACTION_SPAWN_TERMINAL] = "spawn-terminal", [BIND_ACTION_MINIMIZE] = "minimize", [BIND_ACTION_MAXIMIZE] = "maximize", [BIND_ACTION_FULLSCREEN] = "fullscreen", [BIND_ACTION_PIPE_SCROLLBACK] = "pipe-scrollback", [BIND_ACTION_PIPE_VIEW] = "pipe-visible", [BIND_ACTION_PIPE_SELECTED] = "pipe-selected", [BIND_ACTION_PIPE_COMMAND_OUTPUT] = "pipe-command-output", [BIND_ACTION_SHOW_URLS_COPY] = "show-urls-copy", [BIND_ACTION_SHOW_URLS_LAUNCH] = "show-urls-launch", [BIND_ACTION_SHOW_URLS_PERSISTENT] = "show-urls-persistent", [BIND_ACTION_TEXT_BINDING] = "text-binding", [BIND_ACTION_PROMPT_PREV] = "prompt-prev", [BIND_ACTION_PROMPT_NEXT] = "prompt-next", [BIND_ACTION_UNICODE_INPUT] = "unicode-input", [BIND_ACTION_QUIT] = "quit", [BIND_ACTION_REGEX_LAUNCH] = "regex-launch", [BIND_ACTION_REGEX_COPY] = "regex-copy", /* Mouse-specific actions */ [BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse", [BIND_ACTION_SCROLLBACK_DOWN_MOUSE] = "scrollback-down-mouse", [BIND_ACTION_SELECT_BEGIN] = "select-begin", [BIND_ACTION_SELECT_BEGIN_BLOCK] = "select-begin-block", [BIND_ACTION_SELECT_EXTEND] = "select-extend", [BIND_ACTION_SELECT_EXTEND_CHAR_WISE] = "select-extend-character-wise", [BIND_ACTION_SELECT_WORD] = "select-word", [BIND_ACTION_SELECT_WORD_WS] = "select-word-whitespace", [BIND_ACTION_SELECT_QUOTE] = "select-quote", [BIND_ACTION_SELECT_ROW] = "select-row", }; static const char *const search_binding_action_map[] = { [BIND_ACTION_SEARCH_NONE] = NULL, [BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE] = "scrollback-up-page", [BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE] = "scrollback-up-half-page", [BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE] = "scrollback-up-line", [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE] = "scrollback-down-page", [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE] = "scrollback-down-half-page", [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE] = "scrollback-down-line", [BIND_ACTION_SEARCH_SCROLLBACK_HOME] = "scrollback-home", [BIND_ACTION_SEARCH_SCROLLBACK_END] = "scrollback-end", [BIND_ACTION_SEARCH_CANCEL] = "cancel", [BIND_ACTION_SEARCH_COMMIT] = "commit", [BIND_ACTION_SEARCH_FIND_PREV] = "find-prev", [BIND_ACTION_SEARCH_FIND_NEXT] = "find-next", [BIND_ACTION_SEARCH_EDIT_LEFT] = "cursor-left", [BIND_ACTION_SEARCH_EDIT_LEFT_WORD] = "cursor-left-word", [BIND_ACTION_SEARCH_EDIT_RIGHT] = "cursor-right", [BIND_ACTION_SEARCH_EDIT_RIGHT_WORD] = "cursor-right-word", [BIND_ACTION_SEARCH_EDIT_HOME] = "cursor-home", [BIND_ACTION_SEARCH_EDIT_END] = "cursor-end", [BIND_ACTION_SEARCH_DELETE_PREV] = "delete-prev", [BIND_ACTION_SEARCH_DELETE_PREV_WORD] = "delete-prev-word", [BIND_ACTION_SEARCH_DELETE_NEXT] = "delete-next", [BIND_ACTION_SEARCH_DELETE_NEXT_WORD] = "delete-next-word", [BIND_ACTION_SEARCH_DELETE_TO_START] = "delete-to-start", [BIND_ACTION_SEARCH_DELETE_TO_END] = "delete-to-end", [BIND_ACTION_SEARCH_EXTEND_CHAR] = "extend-char", [BIND_ACTION_SEARCH_EXTEND_WORD] = "extend-to-word-boundary", [BIND_ACTION_SEARCH_EXTEND_WORD_WS] = "extend-to-next-whitespace", [BIND_ACTION_SEARCH_EXTEND_LINE_DOWN] = "extend-line-down", [BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR] = "extend-backward-char", [BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD] = "extend-backward-to-word-boundary", [BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS] = "extend-backward-to-next-whitespace", [BIND_ACTION_SEARCH_EXTEND_LINE_UP] = "extend-line-up", [BIND_ACTION_SEARCH_CLIPBOARD_PASTE] = "clipboard-paste", [BIND_ACTION_SEARCH_PRIMARY_PASTE] = "primary-paste", [BIND_ACTION_SEARCH_UNICODE_INPUT] = "unicode-input", }; static const char *const url_binding_action_map[] = { [BIND_ACTION_URL_NONE] = NULL, [BIND_ACTION_URL_CANCEL] = "cancel", [BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL] = "toggle-url-visible", }; static_assert(ALEN(binding_action_map) == BIND_ACTION_COUNT, "binding action map size mismatch"); static_assert(ALEN(search_binding_action_map) == BIND_ACTION_SEARCH_COUNT, "search binding action map size mismatch"); static_assert(ALEN(url_binding_action_map) == BIND_ACTION_URL_COUNT, "URL binding action map size mismatch"); struct context { struct config *conf; const char *section; const char *section_suffix; const char *key; const char *value; const char *path; unsigned lineno; bool errors_are_fatal; }; static const enum user_notification_kind log_class_to_notify_kind[LOG_CLASS_COUNT] = { [LOG_CLASS_WARNING] = USER_NOTIFICATION_WARNING, [LOG_CLASS_ERROR] = USER_NOTIFICATION_ERROR, }; static void NOINLINE VPRINTF(5) log_and_notify_va(struct config *conf, enum log_class log_class, const char *file, int lineno, const char *fmt, va_list va) { xassert(log_class < ALEN(log_class_to_notify_kind)); enum user_notification_kind kind = log_class_to_notify_kind[log_class]; if (kind == 0) { BUG("unsupported log class: %d", (int)log_class); return; } char *formatted_msg = xvasprintf(fmt, va); log_msg(log_class, LOG_MODULE, file, lineno, "%s", formatted_msg); user_notification_add(&conf->notifications, kind, formatted_msg); } static void NOINLINE PRINTF(5) log_and_notify(struct config *conf, enum log_class log_class, const char *file, int lineno, const char *fmt, ...) { va_list va; va_start(va, fmt); log_and_notify_va(conf, log_class, file, lineno, fmt, va); va_end(va); } static void NOINLINE PRINTF(5) log_contextual(struct context *ctx, enum log_class log_class, const char *file, int lineno, const char *fmt, ...) { va_list va; va_start(va, fmt); char *formatted_msg = xvasprintf(fmt, va); va_end(va); const bool print_dot = ctx->key != NULL; const bool print_colon = ctx->value != NULL; const bool print_section_suffix = ctx->section_suffix != NULL; if (!print_dot) ctx->key = ""; if (!print_colon) ctx->value = ""; if (!print_section_suffix) ctx->section_suffix = ""; log_and_notify( ctx->conf, log_class, file, lineno, "%s:%d: [%s%s%s]%s%s%s%s: %s", ctx->path, ctx->lineno, ctx->section, print_section_suffix ? ":" : "", ctx->section_suffix, print_dot ? "." : "", ctx->key, print_colon ? ": " : "", ctx->value, formatted_msg); free(formatted_msg); } static void NOINLINE VPRINTF(4) log_and_notify_errno_va(struct config *conf, const char *file, int lineno, const char *fmt, va_list va) { int errno_copy = errno; char *formatted_msg = xvasprintf(fmt, va); log_and_notify( conf, LOG_CLASS_ERROR, file, lineno, "%s: %s", formatted_msg, strerror(errno_copy)); free(formatted_msg); } static void NOINLINE PRINTF(4) log_and_notify_errno(struct config *conf, const char *file, int lineno, const char *fmt, ...) { va_list va; va_start(va, fmt); log_and_notify_errno_va(conf, file, lineno, fmt, va); va_end(va); } static void NOINLINE PRINTF(4) log_contextual_errno(struct context *ctx, const char *file, int lineno, const char *fmt, ...) { va_list va; va_start(va, fmt); char *formatted_msg = xvasprintf(fmt, va); va_end(va); bool print_dot = ctx->key != NULL; bool print_colon = ctx->value != NULL; if (!print_dot) ctx->key = ""; if (!print_colon) ctx->value = ""; log_and_notify_errno( ctx->conf, file, lineno, "%s:%d: [%s]%s%s%s%s: %s", ctx->path, ctx->lineno, ctx->section, print_dot ? "." : "", ctx->key, print_colon ? ": " : "", ctx->value, formatted_msg); free(formatted_msg); } #define LOG_CONTEXTUAL_ERR(...) \ log_contextual(ctx, LOG_CLASS_ERROR, __FILE__, __LINE__, __VA_ARGS__) #define LOG_CONTEXTUAL_WARN(...) \ log_contextual(ctx, LOG_CLASS_WARNING, __FILE__, __LINE__, __VA_ARGS__) #define LOG_CONTEXTUAL_ERRNO(...) \ log_contextual_errno(ctx, __FILE__, __LINE__, __VA_ARGS__) #define LOG_AND_NOTIFY_ERR(...) \ log_and_notify(conf, LOG_CLASS_ERROR, __FILE__, __LINE__, __VA_ARGS__) #define LOG_AND_NOTIFY_WARN(...) \ log_and_notify(conf, LOG_CLASS_WARNING, __FILE__, __LINE__, __VA_ARGS__) #define LOG_AND_NOTIFY_ERRNO(...) \ log_and_notify_errno(conf, __FILE__, __LINE__, __VA_ARGS__) static char * get_shell(void) { const char *shell = getenv("SHELL"); if (shell == NULL) { struct passwd *passwd = getpwuid(getuid()); if (passwd == NULL) { LOG_ERRNO("failed to lookup user: falling back to 'sh'"); shell = "sh"; } else shell = passwd->pw_shell; } LOG_DBG("user's shell: %s", shell); return xstrdup(shell); } struct config_file { char *path; /* Full, absolute, path */ int fd; /* FD of file, O_RDONLY */ }; static struct config_file open_config(void) { char *path = NULL; struct config_file ret = {.path = NULL, .fd = -1}; const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); const char *xdg_config_dirs = getenv("XDG_CONFIG_DIRS"); const char *home_dir = getenv("HOME"); char *xdg_config_dirs_copy = NULL; /* First, check XDG_CONFIG_HOME (or .config, if unset) */ if (xdg_config_home != NULL && xdg_config_home[0] != '\0') path = xstrjoin(xdg_config_home, "/foot/foot.ini"); else if (home_dir != NULL) path = xstrjoin(home_dir, "/.config/foot/foot.ini"); if (path != NULL) { LOG_DBG("checking for %s", path); int fd = open(path, O_RDONLY | O_CLOEXEC); if (fd >= 0) { ret = (struct config_file) {.path = path, .fd = fd}; path = NULL; goto done; } } xdg_config_dirs_copy = xdg_config_dirs != NULL && xdg_config_dirs[0] != '\0' ? strdup(xdg_config_dirs) : strdup("/etc/xdg"); if (xdg_config_dirs_copy == NULL || xdg_config_dirs_copy[0] == '\0') goto done; for (const char *conf_dir = strtok(xdg_config_dirs_copy, ":"); conf_dir != NULL; conf_dir = strtok(NULL, ":")) { free(path); path = xstrjoin(conf_dir, "/foot/foot.ini"); LOG_DBG("checking for %s", path); int fd = open(path, O_RDONLY | O_CLOEXEC); if (fd >= 0) { ret = (struct config_file){.path = path, .fd = fd}; path = NULL; goto done; } } done: free(xdg_config_dirs_copy); free(path); return ret; } static bool str_has_prefix(const char *str, const char *prefix) { return strncmp(str, prefix, strlen(prefix)) == 0; } static bool NOINLINE value_to_bool(struct context *ctx, bool *res) { static const char *const yes[] = {"on", "true", "yes", "1"}; static const char *const no[] = {"off", "false", "no", "0"}; for (size_t i = 0; i < ALEN(yes); i++) { if (strcasecmp(ctx->value, yes[i]) == 0) { *res = true; return true; } } for (size_t i = 0; i < ALEN(no); i++) { if (strcasecmp(ctx->value, no[i]) == 0) { *res = false; return true; } } LOG_CONTEXTUAL_ERR("invalid boolean value"); return false; } static bool NOINLINE str_to_ulong(const char *s, int base, unsigned long *res) { if (s == NULL) return false; errno = 0; char *end = NULL; *res = strtoul(s, &end, base); return errno == 0 && *end == '\0'; } static bool NOINLINE str_to_uint32(const char *s, int base, uint32_t *res) { unsigned long v; bool ret = str_to_ulong(s, base, &v); if (v > UINT32_MAX) return false; *res = v; return ret; } static bool NOINLINE str_to_uint16(const char *s, int base, uint16_t *res) { unsigned long v; bool ret = str_to_ulong(s, base, &v); if (v > UINT16_MAX) return false; *res = v; return ret; } static bool NOINLINE value_to_uint16(struct context *ctx, int base, uint16_t *res) { if (!str_to_uint16(ctx->value, base, res)) { LOG_CONTEXTUAL_ERR( "invalid integer value, or outside range 0-%u", UINT16_MAX); return false; } return true; } static bool NOINLINE value_to_uint32(struct context *ctx, int base, uint32_t *res) { if (!str_to_uint32(ctx->value, base, res)){ LOG_CONTEXTUAL_ERR( "invalid integer value, or outside range 0-%u", UINT32_MAX); return false; } return true; } static bool NOINLINE value_to_dimensions(struct context *ctx, uint32_t *x, uint32_t *y) { if (sscanf(ctx->value, "%ux%u", x, y) != 2) { LOG_CONTEXTUAL_ERR("invalid dimensions (must be in the form AxB)"); return false; } return true; } static bool NOINLINE value_to_float(struct context *ctx, float *res) { const char *s = ctx->value; if (s == NULL) return false; errno = 0; char *end = NULL; *res = strtof(s, &end); if (!(errno == 0 && *end == '\0')) { LOG_CONTEXTUAL_ERR("invalid decimal value"); return false; } return true; } static bool NOINLINE value_to_str(struct context *ctx, char **res) { char *copy = xstrdup(ctx->value); char *end = copy + strlen(copy) - 1; /* Un-quote * * Note: this is very simple; we only support the *entire* value * being quoted. That is, no mid-value quotes. Both double and * single quotes are supported. * * - key="value" OK * - key=abc "quote" def NOT OK * - key='value' OK * * Finally, we support escaping the quote character, and the * escape character itself: * * - key="value \"quotes\"" * - key="backslash: \\" * * ONLY the "current" quote character can be escaped: * * key="value \'" NOt OK (both backslash and single quote is kept) */ if ((copy[0] == '"' && *end == '"') || (copy[0] == '\'' && *end == '\'')) { const char quote = copy[0]; *end = '\0'; memmove(copy, copy + 1, end - copy); /* Un-escape */ for (char *p = copy; *p != '\0'; p++) { if (p[0] == '\\' && (p[1] == '\\' || p[1] == quote)) { memmove(p, p + 1, end - p); } } } free(*res); *res = copy; return true; } static bool NOINLINE value_to_wchars(struct context *ctx, char32_t **res) { char32_t *s = ambstoc32(ctx->value); if (s == NULL) { LOG_CONTEXTUAL_ERR("not a valid string value"); return false; } free(*res); *res = s; return true; } static bool NOINLINE value_to_enum(struct context *ctx, const char **value_map, int *res) { size_t str_len = 0; size_t count = 0; for (; value_map[count] != NULL; count++) { if (strcasecmp(value_map[count], ctx->value) == 0) { *res = count; return true; } str_len += strlen(value_map[count]); } const size_t size = str_len + count * 4 + 1; char valid_values[512]; size_t idx = 0; xassert(size < sizeof(valid_values)); for (size_t i = 0; i < count; i++) idx += xsnprintf(&valid_values[idx], size - idx, "'%s', ", value_map[i]); if (count > 0) valid_values[idx - 2] = '\0'; LOG_CONTEXTUAL_ERR("not one of %s", valid_values); *res = -1; return false; } static bool NOINLINE value_to_color(struct context *ctx, uint32_t *result, bool allow_alpha) { uint32_t color; const size_t len = strlen(ctx->value); const size_t component_count = len / 2; if (!(len == 6 || (allow_alpha && len == 8)) || !str_to_uint32(ctx->value, 16, &color)) { if (allow_alpha) { LOG_CONTEXTUAL_ERR("color must be in either RGB or ARGB format"); } else { LOG_CONTEXTUAL_ERR("color must be in RGB format"); } return false; } if (allow_alpha && component_count == 3) { /* If user left out the alpha component, assume non-transparency */ color |= 0xff000000; } *result = color; return true; } static bool NOINLINE value_to_two_colors(struct context *ctx, uint32_t *first, uint32_t *second, bool allow_alpha) { bool ret = false; const char *original_value = ctx->value; /* TODO: do this without strdup() */ char *value_copy = xstrdup(ctx->value); const char *first_as_str = strtok(value_copy, " "); const char *second_as_str = strtok(NULL, " "); if (first_as_str == NULL || second_as_str == NULL) { LOG_CONTEXTUAL_ERR("invalid double color value"); goto out; } ctx->value = first_as_str; if (!value_to_color(ctx, first, allow_alpha)) goto out; ctx->value = second_as_str; if (!value_to_color(ctx, second, allow_alpha)) goto out; ret = true; out: free(value_copy); ctx->value = original_value; return ret; } static bool NOINLINE value_to_pt_or_px(struct context *ctx, struct pt_or_px *res) { const char *s = ctx->value; size_t len = s != NULL ? strlen(s) : 0; if (len >= 2 && s[len - 2] == 'p' && s[len - 1] == 'x') { errno = 0; char *end = NULL; long value = strtol(s, &end, 10); if (!(len > 2 && errno == 0 && end == s + len - 2)) { LOG_CONTEXTUAL_ERR("invalid px value (must be in the form 12px)"); return false; } res->pt = 0; res->px = value; } else { float value; if (!value_to_float(ctx, &value)) return false; res->pt = value; res->px = 0; } return true; } static struct config_font_list NOINLINE value_to_fonts(struct context *ctx) { size_t count = 0; size_t size = 0; struct config_font *fonts = NULL; char *copy = xstrdup(ctx->value); for (const char *font = strtok(copy, ","); font != NULL; font = strtok(NULL, ",")) { /* Trim spaces, strictly speaking not necessary, but looks nice :) */ while (isspace(font[0])) font++; if (font[0] == '\0') continue; struct config_font font_data; if (!config_font_parse(font, &font_data)) { ctx->value = font; LOG_CONTEXTUAL_ERR("invalid font specification"); goto err; } if (count + 1 > size) { size += 4; fonts = xrealloc(fonts, size * sizeof(fonts[0])); } xassert(count + 1 <= size); fonts[count++] = font_data; } free(copy); return (struct config_font_list){.arr = fonts, .count = count}; err: free(copy); free(fonts); return (struct config_font_list){.arr = NULL, .count = 0}; } static void NOINLINE free_argv(struct argv *argv) { if (argv->args == NULL) return; for (char **a = argv->args; *a != NULL; a++) free(*a); free(argv->args); argv->args = NULL; } static void NOINLINE clone_argv(struct argv *dst, const struct argv *src) { if (src->args == NULL) { dst->args = NULL; return; } size_t count = 0; for (char **args = src->args; *args != NULL; args++) count++; dst->args = xmalloc((count + 1) * sizeof(dst->args[0])); for (char **args_src = src->args, **args_dst = dst->args; *args_src != NULL; args_src++, args_dst++) { *args_dst = xstrdup(*args_src); } dst->args[count] = NULL; } static void spawn_template_free(struct config_spawn_template *template) { free_argv(&template->argv); } static void spawn_template_clone(struct config_spawn_template *dst, const struct config_spawn_template *src) { clone_argv(&dst->argv, &src->argv); } static bool NOINLINE value_to_spawn_template(struct context *ctx, struct config_spawn_template *template) { spawn_template_free(template); char **argv = NULL; if (ctx->value[0] == '"' && ctx->value[1] == '"' && ctx->value[2] == '\0') { template->argv.args = NULL; return true; } if (!tokenize_cmdline(ctx->value, &argv)) { LOG_CONTEXTUAL_ERR("syntax error in command line"); return false; } template->argv.args = argv; return true; } static bool parse_config_file( FILE *f, struct config *conf, const char *path, bool errors_are_fatal); static bool parse_section_main(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; const char *value = ctx->value; bool errors_are_fatal = ctx->errors_are_fatal; if (streq(key, "include")) { char *_include_path = NULL; const char *include_path = NULL; if (value[0] == '~' && value[1] == '/') { const char *home_dir = getenv("HOME"); if (home_dir == NULL) { LOG_CONTEXTUAL_ERRNO("failed to expand '~'"); return false; } _include_path = xstrjoin3(home_dir, "/", value + 2); include_path = _include_path; } else include_path = value; if (include_path[0] != '/') { LOG_CONTEXTUAL_ERR("not an absolute path"); free(_include_path); return false; } FILE *include = fopen(include_path, "r"); if (include == NULL) { LOG_CONTEXTUAL_ERRNO("failed to open"); free(_include_path); return false; } bool ret = parse_config_file( include, conf, include_path, errors_are_fatal); fclose(include); LOG_INFO("imported sub-configuration from %s", include_path); free(_include_path); return ret; } else if (streq(key, "term")) return value_to_str(ctx, &conf->term); else if (streq(key, "shell")) return value_to_str(ctx, &conf->shell); else if (streq(key, "login-shell")) return value_to_bool(ctx, &conf->login_shell); else if (streq(key, "title")) return value_to_str(ctx, &conf->title); else if (streq(key, "locked-title")) return value_to_bool(ctx, &conf->locked_title); else if (streq(key, "app-id")) return value_to_str(ctx, &conf->app_id); else if (streq(key, "initial-window-size-pixels")) { if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height)) return false; conf->size.type = CONF_SIZE_PX; return true; } else if (streq(key, "initial-window-size-chars")) { if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height)) return false; conf->size.type = CONF_SIZE_CELLS; return true; } else if (streq(key, "pad")) { unsigned x, y; char mode[16] = {0}; 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 != 2 && ret != 3) || invalid_mode) { LOG_CONTEXTUAL_ERR( "invalid padding (must be in the form PAD_XxPAD_Y [center])"); return false; } conf->pad_x = x; conf->pad_y = y; conf->center = center; return true; } else if (streq(key, "resize-delay-ms")) return value_to_uint16(ctx, 10, &conf->resize_delay_ms); else if (streq(key, "resize-by-cells")) return value_to_bool(ctx, &conf->resize_by_cells); else if (streq(key, "resize-keep-grid")) return value_to_bool(ctx, &conf->resize_keep_grid); else if (streq(key, "bold-text-in-bright")) { if (streq(value, "palette-based")) { conf->bold_in_bright.enabled = true; conf->bold_in_bright.palette_based = true; } else { if (!value_to_bool(ctx, &conf->bold_in_bright.enabled)) return false; conf->bold_in_bright.palette_based = false; } return true; } else if (streq(key, "initial-window-mode")) { _Static_assert(sizeof(conf->startup_mode) == sizeof(int), "enum is not 32-bit"); return value_to_enum( ctx, (const char *[]){"windowed", "maximized", "fullscreen", NULL}, (int *)&conf->startup_mode); } else if (streq(key, "font") || streq(key, "font-bold") || streq(key, "font-italic") || streq(key, "font-bold-italic")) { size_t idx = streq(key, "font") ? 0 : streq(key, "font-bold") ? 1 : streq(key, "font-italic") ? 2 : 3; struct config_font_list new_list = value_to_fonts(ctx); if (new_list.arr == NULL) return false; config_font_list_destroy(&conf->fonts[idx]); conf->fonts[idx] = new_list; return true; } else if (streq(key, "font-size-adjustment")) { const size_t len = strlen(ctx->value); if (len >= 1 && ctx->value[len - 1] == '%') { errno = 0; char *end = NULL; float percent = strtof(ctx->value, &end); if (!(len > 1 && errno == 0 && end == ctx->value + len - 1)) { LOG_CONTEXTUAL_ERR( "invalid percent value (must be in the form 10.5%%)"); return false; } conf->font_size_adjustment.percent = percent / 100.; conf->font_size_adjustment.pt_or_px.pt = 0; conf->font_size_adjustment.pt_or_px.px = 0; return true; } else { bool ret = value_to_pt_or_px(ctx, &conf->font_size_adjustment.pt_or_px); if (ret) conf->font_size_adjustment.percent = 0.; return ret; } } else if (streq(key, "line-height")) return value_to_pt_or_px(ctx, &conf->line_height); else if (streq(key, "letter-spacing")) return value_to_pt_or_px(ctx, &conf->letter_spacing); else if (streq(key, "horizontal-letter-offset")) return value_to_pt_or_px(ctx, &conf->horizontal_letter_offset); else if (streq(key, "vertical-letter-offset")) return value_to_pt_or_px(ctx, &conf->vertical_letter_offset); else if (streq(key, "underline-offset")) { if (!value_to_pt_or_px(ctx, &conf->underline_offset)) return false; conf->use_custom_underline_offset = true; return true; } else if (streq(key, "underline-thickness")) return value_to_pt_or_px(ctx, &conf->underline_thickness); else if (streq(key, "strikeout-thickness")) return value_to_pt_or_px(ctx, &conf->strikeout_thickness); else if (streq(key, "dpi-aware")) return value_to_bool(ctx, &conf->dpi_aware); else if (streq(key, "workers")) return value_to_uint16(ctx, 10, &conf->render_worker_count); else if (streq(key, "word-delimiters")) return value_to_wchars(ctx, &conf->word_delimiters); else if (streq(key, "selection-target")) { _Static_assert(sizeof(conf->selection_target) == sizeof(int), "enum is not 32-bit"); return value_to_enum( ctx, (const char *[]){"none", "primary", "clipboard", "both", NULL}, (int *)&conf->selection_target); } else if (streq(key, "box-drawings-uses-font-glyphs")) return value_to_bool(ctx, &conf->box_drawings_uses_font_glyphs); else if (streq(key, "utmp-helper")) { if (!value_to_str(ctx, &conf->utmp_helper_path)) return false; if (streq(conf->utmp_helper_path, "none")) { free(conf->utmp_helper_path); conf->utmp_helper_path = NULL; } return true; } else if (streq(key, "gamma-correct-blending")) { bool gamma_correct; if (!value_to_bool(ctx, &gamma_correct)) return false; conf->gamma_correct = gamma_correct ? GAMMA_CORRECT_ENABLED : GAMMA_CORRECT_DISABLED; return true; } else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; } } static bool parse_section_security(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; if (streq(key, "osc52")) { _Static_assert(sizeof(conf->security.osc52) == sizeof(int), "enum is not 32-bit"); return value_to_enum( ctx, (const char *[]){"disabled", "copy-enabled", "paste-enabled", "enabled", NULL}, (int *)&conf->security.osc52); } else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; } } static bool parse_section_bell(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; if (streq(key, "urgent")) return value_to_bool(ctx, &conf->bell.urgent); else if (streq(key, "notify")) return value_to_bool(ctx, &conf->bell.notify); else if (streq(key, "system")) return value_to_bool(ctx, &conf->bell.system_bell); else if (streq(key, "visual")) return value_to_bool(ctx, &conf->bell.flash); else if (streq(key, "command")) return value_to_spawn_template(ctx, &conf->bell.command); else if (streq(key, "command-focused")) return value_to_bool(ctx, &conf->bell.command_focused); else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; } } static bool parse_section_desktop_notifications(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; if (streq(key, "command")) return value_to_spawn_template( ctx, &conf->desktop_notifications.command); else if (streq(key, "command-action-argument")) return value_to_spawn_template( ctx, &conf->desktop_notifications.command_action_arg); else if (streq(key, "close")) return value_to_spawn_template( ctx, &conf->desktop_notifications.close); else if (streq(key, "inhibit-when-focused")) return value_to_bool( ctx, &conf->desktop_notifications.inhibit_when_focused); else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; } } static bool parse_section_scrollback(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; const char *value = ctx->value; if (streq(key, "lines")) return value_to_uint32(ctx, 10, &conf->scrollback.lines); else if (streq(key, "indicator-position")) { _Static_assert( sizeof(conf->scrollback.indicator.position) == sizeof(int), "enum is not 32-bit"); return value_to_enum( ctx, (const char *[]){"none", "fixed", "relative", NULL}, (int *)&conf->scrollback.indicator.position); } else if (streq(key, "indicator-format")) { if (streq(value, "percentage")) { conf->scrollback.indicator.format = SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE; return true; } else if (streq(value, "line")) { conf->scrollback.indicator.format = SCROLLBACK_INDICATOR_FORMAT_LINENO; return true; } else return value_to_wchars(ctx, &conf->scrollback.indicator.text); } else if (streq(key, "multiplier")) return value_to_float(ctx, &conf->scrollback.multiplier); else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; } } static bool parse_section_url(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; if (streq(key, "launch")) return value_to_spawn_template(ctx, &conf->url.launch); else if (streq(key, "label-letters")) return value_to_wchars(ctx, &conf->url.label_letters); else if (streq(key, "osc8-underline")) { _Static_assert(sizeof(conf->url.osc8_underline) == sizeof(int), "enum is not 32-bit"); return value_to_enum( ctx, (const char *[]){"url-mode", "always", NULL}, (int *)&conf->url.osc8_underline); } else if (streq(key, "regex")) { const char *regex = ctx->value; regex_t preg; int r = regcomp(&preg, regex, REG_EXTENDED); if (r != 0) { char err_buf[128]; regerror(r, &preg, err_buf, sizeof(err_buf)); LOG_CONTEXTUAL_ERR("invalid regex: %s", err_buf); return false; } if (preg.re_nsub == 0) { LOG_CONTEXTUAL_ERR("invalid regex: no marked subexpression(s)"); regfree(&preg); return false; } regfree(&conf->url.preg); free(conf->url.regex); conf->url.regex = xstrdup(regex); conf->url.preg = preg; return true; } else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; } } static bool parse_section_regex(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; const char *regex_name = ctx->section_suffix != NULL ? ctx->section_suffix : ""; struct custom_regex *regex = NULL; tll_foreach(conf->custom_regexes, it) { if (streq(it->item.name, regex_name)) { regex = &it->item; break; } } if (streq(key, "regex")) { const char *regex_string = ctx->value; regex_t preg; int r = regcomp(&preg, regex_string, REG_EXTENDED); if (r != 0) { char err_buf[128]; regerror(r, &preg, err_buf, sizeof(err_buf)); LOG_CONTEXTUAL_ERR("invalid regex: %s", err_buf); return false; } if (preg.re_nsub == 0) { LOG_CONTEXTUAL_ERR("invalid regex: no marked subexpression(s)"); regfree(&preg); return false; } if (regex == NULL) { tll_push_back(conf->custom_regexes, ((struct custom_regex){.name = xstrdup(regex_name)})); regex = &tll_back(conf->custom_regexes); } regfree(®ex->preg); free(regex->regex); regex->regex = xstrdup(regex_string); regex->preg = preg; return true; } else if (streq(key, "launch")) { struct config_spawn_template launch = {NULL}; if (!value_to_spawn_template(ctx, &launch)) return false; if (regex == NULL) { tll_push_back(conf->custom_regexes, ((struct custom_regex){.name = xstrdup(regex_name)})); regex = &tll_back(conf->custom_regexes); } spawn_template_free(®ex->launch); regex->launch = launch; return true; } else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; } } static bool parse_section_colors(struct context *ctx) { struct config *conf = ctx->conf; 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)) { LOG_CONTEXTUAL_ERR( "invalid color palette index: %s (not in range 0-%zu)", key, ALEN(conf->colors.table)); return false; } color = &conf->colors.table[index]; } else if (key_len == 8 && str_has_prefix(key, "regular") && last_digit < 8) color = &conf->colors.table[last_digit]; else if (key_len == 7 && str_has_prefix(key, "bright") && last_digit < 8) color = &conf->colors.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)) return false; conf->colors.use_custom.dim |= 1 << last_digit; return true; } else if (str_has_prefix(key, "sixel") && ((key_len == 6 && last_digit < 10) || (key_len == 7 && key[5] == '1' && last_digit < 6))) { size_t idx = key_len == 6 ? last_digit : 10 + last_digit; return value_to_color(ctx, &conf->colors.sixel[idx], false); } else if (streq(key, "flash")) color = &conf->colors.flash; else if (streq(key, "foreground")) color = &conf->colors.fg; else if (streq(key, "background")) color = &conf->colors.bg; else if (streq(key, "selection-foreground")) color = &conf->colors.selection_fg; else if (streq(key, "selection-background")) color = &conf->colors.selection_bg; else if (streq(key, "jump-labels")) { if (!value_to_two_colors( ctx, &conf->colors.jump_label.fg, &conf->colors.jump_label.bg, false)) { return false; } conf->colors.use_custom.jump_label = true; return true; } else if (streq(key, "scrollback-indicator")) { if (!value_to_two_colors( ctx, &conf->colors.scrollback_indicator.fg, &conf->colors.scrollback_indicator.bg, false)) { return false; } conf->colors.use_custom.scrollback_indicator = true; return true; } 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, false)) { return false; } conf->colors.use_custom.search_box_no_match = true; return true; } 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, false)) { return false; } conf->colors.use_custom.search_box_match = true; return true; } else if (streq(key, "urls")) { if (!value_to_color(ctx, &conf->colors.url, false)) return false; conf->colors.use_custom.url = true; return true; } else if (streq(key, "alpha")) { float alpha; if (!value_to_float(ctx, &alpha)) return false; if (alpha < 0. || alpha > 1.) { LOG_CONTEXTUAL_ERR("not in range 0.0-1.0"); return false; } conf->colors.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; } conf->colors.flash_alpha = alpha * 65535.; return true; } else { LOG_CONTEXTUAL_ERR("not valid option"); return false; } uint32_t color_value; if (!value_to_color(ctx, &color_value, false)) return false; *color = color_value; return true; } static bool parse_section_cursor(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; if (streq(key, "style")) { _Static_assert(sizeof(conf->cursor.style) == sizeof(int), "enum is not 32-bit"); return value_to_enum( ctx, (const char *[]){"block", "underline", "beam", "hollow", NULL}, (int *)&conf->cursor.style); } else if (streq(key, "unfocused-style")) { _Static_assert(sizeof(conf->cursor.unfocused_style) == sizeof(int), "enum is not 32-bit"); return value_to_enum( ctx, (const char *[]){"unchanged", "hollow", "none", NULL}, (int *)&conf->cursor.unfocused_style); } else if (streq(key, "blink")) return value_to_bool(ctx, &conf->cursor.blink.enabled); else if (streq(key, "blink-rate")) return value_to_uint32(ctx, 10, &conf->cursor.blink.rate_ms); else if (streq(key, "color")) { 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; } else if (streq(key, "beam-thickness")) return value_to_pt_or_px(ctx, &conf->cursor.beam_thickness); else if (streq(key, "underline-thickness")) return value_to_pt_or_px(ctx, &conf->cursor.underline_thickness); else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; } } static bool parse_section_mouse(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; if (streq(key, "hide-when-typing")) return value_to_bool(ctx, &conf->mouse.hide_when_typing); else if (streq(key, "alternate-scroll-mode")) return value_to_bool(ctx, &conf->mouse.alternate_scroll_mode); else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; } } static bool parse_section_csd(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; if (streq(key, "preferred")) { _Static_assert(sizeof(conf->csd.preferred) == sizeof(int), "enum is not 32-bit"); return value_to_enum( ctx, (const char *[]){"none", "server", "client", NULL}, (int *)&conf->csd.preferred); } else if (streq(key, "font")) { struct config_font_list new_list = value_to_fonts(ctx); if (new_list.arr == NULL) return false; config_font_list_destroy(&conf->csd.font); conf->csd.font = new_list; return true; } else if (streq(key, "color")) { uint32_t color; if (!value_to_color(ctx, &color, true)) return false; conf->csd.color.title_set = true; conf->csd.color.title = color; return true; } else if (streq(key, "size")) return value_to_uint16(ctx, 10, &conf->csd.title_height); else if (streq(key, "button-width")) return value_to_uint16(ctx, 10, &conf->csd.button_width); else if (streq(key, "button-color")) { if (!value_to_color(ctx, &conf->csd.color.buttons, true)) return false; conf->csd.color.buttons_set = true; return true; } else if (streq(key, "button-minimize-color")) { if (!value_to_color(ctx, &conf->csd.color.minimize, true)) return false; conf->csd.color.minimize_set = true; return true; } else if (streq(key, "button-maximize-color")) { if (!value_to_color(ctx, &conf->csd.color.maximize, true)) return false; conf->csd.color.maximize_set = true; return true; } else if (streq(key, "button-close-color")) { if (!value_to_color(ctx, &conf->csd.color.quit, true)) return false; conf->csd.color.close_set = true; return true; } else if (streq(key, "border-color")) { if (!value_to_color(ctx, &conf->csd.color.border, true)) return false; conf->csd.color.border_set = true; return true; } else if (streq(key, "border-width")) return value_to_uint16(ctx, 10, &conf->csd.border_width_visible); else if (streq(key, "hide-when-maximized")) return value_to_bool(ctx, &conf->csd.hide_when_maximized); else if (streq(key, "double-click-to-maximize")) return value_to_bool(ctx, &conf->csd.double_click_to_maximize); else { LOG_CONTEXTUAL_ERR("not a valid action: %s", key); return false; } } static void free_binding_aux(struct binding_aux *aux) { if (!aux->master_copy) return; switch (aux->type) { case BINDING_AUX_NONE: break; case BINDING_AUX_PIPE: free_argv(&aux->pipe); break; case BINDING_AUX_TEXT: free(aux->text.data); break; case BINDING_AUX_REGEX: free(aux->regex_name); break; } } static void free_key_binding(struct config_key_binding *binding) { free_binding_aux(&binding->aux); tll_free_and_free(binding->modifiers, free); } static void NOINLINE free_key_binding_list(struct config_key_binding_list *bindings) { struct config_key_binding *binding = &bindings->arr[0]; for (size_t i = 0; i < bindings->count; i++, binding++) free_key_binding(binding); free(bindings->arr); bindings->arr = NULL; bindings->count = 0; } static void NOINLINE parse_modifiers(const char *text, size_t len, config_modifier_list_t *modifiers) { tll_free_and_free(*modifiers, free); /* Handle "none" separately because e.g. none+shift is nonsense */ if (strncmp(text, "none", len) == 0) return; char *copy = xstrndup(text, len); for (char *ctx = NULL, *key = strtok_r(copy, "+", &ctx); key != NULL; key = strtok_r(NULL, "+", &ctx)) { tll_push_back(*modifiers, xstrdup(key)); } free(copy); tll_sort(*modifiers, strcmp); } static int NOINLINE argv_compare(const struct argv *argv1, const struct argv *argv2) { if (argv1->args == NULL && argv2->args == NULL) return 0; if (argv1->args == NULL) return -1; if (argv2->args == NULL) return 1; for (size_t i = 0; ; i++) { if (argv1->args[i] == NULL && argv2->args[i] == NULL) return 0; if (argv1->args[i] == NULL) return -1; if (argv2->args[i] == NULL) return 1; int ret = strcmp(argv1->args[i], argv2->args[i]); if (ret != 0) return ret; } BUG("unexpected loop break"); return 1; } static bool NOINLINE binding_aux_equal(const struct binding_aux *a, const struct binding_aux *b) { if (a->type != b->type) return false; switch (a->type) { case BINDING_AUX_NONE: return true; case BINDING_AUX_PIPE: return argv_compare(&a->pipe, &b->pipe) == 0; case BINDING_AUX_TEXT: return a->text.len == b->text.len && memcmp(a->text.data, b->text.data, a->text.len) == 0; case BINDING_AUX_REGEX: return streq(a->regex_name, b->regex_name); } BUG("invalid AUX type: %d", a->type); return false; } static void NOINLINE remove_from_key_bindings_list(struct config_key_binding_list *bindings, int action, const struct binding_aux *aux) { size_t remove_first_idx = 0; size_t remove_count = 0; for (size_t i = 0; i < bindings->count; i++) { struct config_key_binding *binding = &bindings->arr[i]; if (binding->action != action) continue; if (binding_aux_equal(&binding->aux, aux)) { if (remove_count++ == 0) remove_first_idx = i; xassert(remove_first_idx + remove_count - 1 == i); free_key_binding(binding); } } if (remove_count == 0) return; size_t move_count = bindings->count - (remove_first_idx + remove_count); memmove( &bindings->arr[remove_first_idx], &bindings->arr[remove_first_idx + remove_count], move_count * sizeof(bindings->arr[0])); bindings->count -= remove_count; } static const struct { const char *name; int code; } button_map[] = { /* System defined */ {"BTN_LEFT", BTN_LEFT}, {"BTN_RIGHT", BTN_RIGHT}, {"BTN_MIDDLE", BTN_MIDDLE}, {"BTN_SIDE", BTN_SIDE}, {"BTN_EXTRA", BTN_EXTRA}, {"BTN_FORWARD", BTN_FORWARD}, {"BTN_BACK", BTN_BACK}, {"BTN_TASK", BTN_TASK}, /* Foot custom, to be able to map scroll events to mouse bindings */ {"BTN_WHEEL_BACK", BTN_WHEEL_BACK}, {"BTN_WHEEL_FORWARD", BTN_WHEEL_FORWARD}, {"BTN_WHEEL_LEFT", BTN_WHEEL_LEFT}, {"BTN_WHEEL_RIGHT", BTN_WHEEL_RIGHT}, }; static int mouse_button_name_to_code(const char *name) { for (size_t i = 0; i < ALEN(button_map); i++) { if (streq(button_map[i].name, name)) return button_map[i].code; } return -1; } static const char* mouse_button_code_to_name(int code) { for (size_t i = 0; i < ALEN(button_map); i++) { if (code == button_map[i].code) return button_map[i].name; } return NULL; } static bool NOINLINE value_to_key_combos(struct context *ctx, int action, struct binding_aux *aux, struct config_key_binding_list *bindings, enum key_binding_type type) { if (strcasecmp(ctx->value, "none") == 0) { remove_from_key_bindings_list(bindings, action, aux); return true; } /* Count number of combinations */ size_t combo_count = 1; size_t used_combos = 1; /* For error handling */ for (const char *p = strchr(ctx->value, ' '); p != NULL; p = strchr(p + 1, ' ')) { combo_count++; } struct config_key_binding new_combos[combo_count]; char *copy = xstrdup(ctx->value); size_t idx = 0; for (char *tok_ctx = NULL, *combo = strtok_r(copy, " ", &tok_ctx); combo != NULL; combo = strtok_r(NULL, " ", &tok_ctx), idx++, used_combos++) { struct config_key_binding *new_combo = &new_combos[idx]; new_combo->action = action; new_combo->aux = *aux; new_combo->aux.master_copy = idx == 0; #if 0 new_combo->aux.type = BINDING_AUX_PIPE; new_combo->aux.master_copy = idx == 0; new_combo->aux.pipe = *argv; #endif memset(&new_combo->modifiers, 0, sizeof(new_combo->modifiers)); new_combo->path = ctx->path; new_combo->lineno = ctx->lineno; char *key = strrchr(combo, '+'); if (key == NULL) { /* No modifiers */ key = combo; } else { *key = '\0'; parse_modifiers(combo, key - combo, &new_combo->modifiers); key++; /* Skip past the '+' */ } switch (type) { case KEY_BINDING: /* Translate key name to symbol */ new_combo->k.sym = xkb_keysym_from_name(key, 0); if (new_combo->k.sym == XKB_KEY_NoSymbol) { LOG_CONTEXTUAL_ERR("not a valid XKB key name: %s", key); goto err; } break; case MOUSE_BINDING: { new_combo->m.count = 1; char *_count = strrchr(key, '-'); if (_count != NULL) { *_count = '\0'; _count++; errno = 0; char *end; unsigned long value = strtoul(_count, &end, 10); if (_count[0] == '\0' || *end != '\0' || errno != 0) { if (errno != 0) LOG_CONTEXTUAL_ERRNO("invalid click count: %s", _count); else LOG_CONTEXTUAL_ERR("invalid click count: %s", _count); goto err; } new_combo->m.count = value; } new_combo->m.button = mouse_button_name_to_code(key); if (new_combo->m.button < 0) { LOG_CONTEXTUAL_ERR("invalid mouse button name: %s", key); goto err; } break; } } } if (idx == 0) { LOG_CONTEXTUAL_ERR( "empty binding not allowed (set to 'none' to unmap)"); free(copy); return false; } remove_from_key_bindings_list(bindings, action, aux); bindings->arr = xrealloc( bindings->arr, (bindings->count + combo_count) * sizeof(bindings->arr[0])); memcpy(&bindings->arr[bindings->count], new_combos, combo_count * sizeof(bindings->arr[0])); bindings->count += combo_count; free(copy); return true; err: for (size_t i = 0; i < used_combos; i++) free_key_binding(&new_combos[i]); free(copy); return false; } static bool modifiers_equal(const config_modifier_list_t *mods1, const config_modifier_list_t *mods2) { if (tll_length(*mods1) != tll_length(*mods2)) return false; size_t count = 0; tll_foreach(*mods1, it1) { size_t skip = count; tll_foreach(*mods2, it2) { if (skip > 0) { skip--; continue; } if (strcmp(it1->item, it2->item) != 0) return false; break; } count++; } return true; /* * bool shift = mods1->shift == mods2->shift; * bool alt = mods1->alt == mods2->alt; * bool ctrl = mods1->ctrl == mods2->ctrl; * bool super = mods1->super == mods2->super; * return shift && alt && ctrl && super; */ } UNITTEST { config_modifier_list_t mods1 = tll_init(); config_modifier_list_t mods2 = tll_init(); tll_push_back(mods1, xstrdup("foo")); tll_push_back(mods1, xstrdup("bar")); tll_push_back(mods2, xstrdup("foo")); xassert(!modifiers_equal(&mods1, &mods2)); tll_push_back(mods2, xstrdup("zoo")); xassert(!modifiers_equal(&mods1, &mods2)); free(tll_pop_back(mods2)); tll_push_back(mods2, xstrdup("bar")); xassert(modifiers_equal(&mods1, &mods2)); tll_free_and_free(mods1, free); tll_free_and_free(mods2, free); } static bool modifiers_disjoint(const config_modifier_list_t *mods1, const config_modifier_list_t *mods2) { return !modifiers_equal(mods1, mods2); } static char * NOINLINE modifiers_to_str(const config_modifier_list_t *mods, bool strip_last_plus) { size_t len = tll_length(*mods); /* '+' separator */ tll_foreach(*mods, it) len += strlen(it->item); char *ret = xmalloc(len + 1); size_t idx = 0; tll_foreach(*mods, it) { idx += snprintf(&ret[idx], len - idx, "%s", it->item); ret[idx++] = '+'; } if (strip_last_plus) idx--; ret[idx] = '\0'; return ret; } /* * Parses a key binding value in the form * "[cmd-to-exec arg1 arg2] Mods+Key" * * and extracts 'cmd-to-exec' and its arguments. * * Input: * - value: raw string, in the form mentioned above * - cmd: pointer to string to will be allocated and filled with * 'cmd-to-exec arg1 arg2' * - argv: point to array of string. Array will be allocated. Will be * filled with {'cmd-to-exec', 'arg1', 'arg2', NULL} * * Returns: * - ssize_t, number of bytes that were stripped from 'value' to remove the '[]' * enclosed cmd and its arguments, including any subsequent * whitespace characters. I.e. if 'value' is "[cmd] BTN_RIGHT", the * return value is 6 (strlen("[cmd] ")). * - cmd: allocated string containing "cmd arg1 arg2...". Caller frees. * - argv: allocated array containing {"cmd", "arg1", "arg2", NULL}. Caller frees. */ static ssize_t NOINLINE pipe_argv_from_value(struct context *ctx, struct argv *argv) { argv->args = NULL; if (ctx->value[0] != '[') return 0; const char *pipe_cmd_end = strrchr(ctx->value, ']'); if (pipe_cmd_end == NULL) { LOG_CONTEXTUAL_ERR("unclosed '['"); return -1; } size_t pipe_len = pipe_cmd_end - ctx->value - 1; char *cmd = xstrndup(&ctx->value[1], pipe_len); if (!tokenize_cmdline(cmd, &argv->args)) { LOG_CONTEXTUAL_ERR("syntax error in command line"); free(cmd); return -1; } ssize_t remove_len = pipe_cmd_end + 1 - ctx->value; ctx->value = pipe_cmd_end + 1; while (isspace(*ctx->value)) { ctx->value++; remove_len++; } free(cmd); return remove_len; } static ssize_t NOINLINE regex_name_from_value(struct context *ctx, char **regex_name) { *regex_name = NULL; if (ctx->value[0] != '[') return 0; const char *regex_end = strrchr(ctx->value, ']'); if (regex_end == NULL) { LOG_CONTEXTUAL_ERR("unclosed '['"); return -1; } size_t regex_len = regex_end - ctx->value - 1; *regex_name = xstrndup(&ctx->value[1], regex_len); ssize_t remove_len = regex_end + 1 - ctx->value; ctx->value = regex_end + 1; while (isspace(*ctx->value)) { ctx->value++; remove_len++; } return remove_len; } static bool NOINLINE parse_key_binding_section(struct context *ctx, int action_count, const char *const action_map[static action_count], struct config_key_binding_list *bindings) { for (int action = 0; action < action_count; action++) { if (action_map[action] == NULL) continue; if (!streq(ctx->key, action_map[action])) continue; struct binding_aux aux = {.type = BINDING_AUX_NONE, .master_copy = true}; /* TODO: this is ugly... */ if (action_map == binding_action_map && action >= BIND_ACTION_PIPE_SCROLLBACK && action <= BIND_ACTION_PIPE_COMMAND_OUTPUT) { ssize_t pipe_remove_len = pipe_argv_from_value(ctx, &aux.pipe); if (pipe_remove_len <= 0) return false; aux.type = BINDING_AUX_PIPE; aux.master_copy = true; } else if (action_map == binding_action_map && action >= BIND_ACTION_REGEX_LAUNCH && action <= BIND_ACTION_REGEX_COPY) { char *regex_name = NULL; ssize_t regex_remove_len = regex_name_from_value(ctx, ®ex_name); if (regex_remove_len <= 0) return false; aux.type = BINDING_AUX_REGEX; aux.master_copy = true; aux.regex_name = regex_name; } if (!value_to_key_combos(ctx, action, &aux, bindings, KEY_BINDING)) { free_binding_aux(&aux); return false; } return true; } LOG_CONTEXTUAL_ERR("not a valid action: %s", ctx->key); return false; } UNITTEST { enum test_actions { TEST_ACTION_NONE, TEST_ACTION_FOO, TEST_ACTION_BAR, TEST_ACTION_COUNT, }; const char *const map[] = { [TEST_ACTION_NONE] = NULL, [TEST_ACTION_FOO] = "foo", [TEST_ACTION_BAR] = "bar", }; struct config conf = {0}; struct config_key_binding_list bindings = {0}; struct context ctx = { .conf = &conf, .section = "", .key = "foo", .value = "Escape", .path = "", }; /* * ADD foo=Escape * * This verifies we can bind a single key combo to an action. */ xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings)); xassert(bindings.count == 1); xassert(bindings.arr[0].action == TEST_ACTION_FOO); xassert(bindings.arr[0].k.sym == XKB_KEY_Escape); /* * ADD bar=Control+g Control+Shift+x * * This verifies we can bind multiple key combos to an action. */ ctx.key = "bar"; ctx.value = "Control+g Control+Shift+x"; xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings)); xassert(bindings.count == 3); xassert(bindings.arr[0].action == TEST_ACTION_FOO); xassert(bindings.arr[1].action == TEST_ACTION_BAR); xassert(bindings.arr[1].k.sym == XKB_KEY_g); xassert(tll_length(bindings.arr[1].modifiers) == 1); xassert(strcmp(tll_front(bindings.arr[1].modifiers), XKB_MOD_NAME_CTRL) == 0); xassert(bindings.arr[2].action == TEST_ACTION_BAR); xassert(bindings.arr[2].k.sym == XKB_KEY_x); xassert(tll_length(bindings.arr[2].modifiers) == 2); xassert(strcmp(tll_front(bindings.arr[2].modifiers), XKB_MOD_NAME_CTRL) == 0); xassert(strcmp(tll_back(bindings.arr[2].modifiers), XKB_MOD_NAME_SHIFT) == 0); /* * REPLACE foo with foo=Mod+v Shift+q * * This verifies we can update a single-combo action with multiple * key combos. */ ctx.key = "foo"; ctx.value = "Mod1+v Shift+q"; xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings)); xassert(bindings.count == 4); xassert(bindings.arr[0].action == TEST_ACTION_BAR); xassert(bindings.arr[1].action == TEST_ACTION_BAR); xassert(bindings.arr[2].action == TEST_ACTION_FOO); xassert(bindings.arr[2].k.sym == XKB_KEY_v); xassert(tll_length(bindings.arr[2].modifiers) == 1); xassert(strcmp(tll_front(bindings.arr[2].modifiers), XKB_MOD_NAME_ALT) == 0); xassert(bindings.arr[3].action == TEST_ACTION_FOO); xassert(bindings.arr[3].k.sym == XKB_KEY_q); xassert(tll_length(bindings.arr[3].modifiers) == 1); xassert(strcmp(tll_front(bindings.arr[3].modifiers), XKB_MOD_NAME_SHIFT) == 0); /* * REMOVE bar */ ctx.key = "bar"; ctx.value = "none"; xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings)); xassert(bindings.count == 2); xassert(bindings.arr[0].action == TEST_ACTION_FOO); xassert(bindings.arr[1].action == TEST_ACTION_FOO); /* * REMOVE foo */ ctx.key = "foo"; ctx.value = "none"; xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings)); xassert(bindings.count == 0); free(bindings.arr); } static bool parse_section_key_bindings(struct context *ctx) { return parse_key_binding_section( ctx, BIND_ACTION_KEY_COUNT, binding_action_map, &ctx->conf->bindings.key); } static bool parse_section_search_bindings(struct context *ctx) { return parse_key_binding_section( ctx, BIND_ACTION_SEARCH_COUNT, search_binding_action_map, &ctx->conf->bindings.search); } static bool parse_section_url_bindings(struct context *ctx) { return parse_key_binding_section( ctx, BIND_ACTION_URL_COUNT, url_binding_action_map, &ctx->conf->bindings.url); } static bool NOINLINE resolve_key_binding_collisions(struct config *conf, const char *section_name, const char *const action_map[], struct config_key_binding_list *bindings, enum key_binding_type type) { bool ret = true; for (size_t i = 1; i < bindings->count; i++) { enum {COLLISION_NONE, COLLISION_OVERRIDE, COLLISION_BINDING} collision_type = COLLISION_NONE; const struct config_key_binding *collision_binding = NULL; struct config_key_binding *binding1 = &bindings->arr[i]; xassert(binding1->action != BIND_ACTION_NONE); const config_modifier_list_t *mods1 = &binding1->modifiers; /* Does our modifiers collide with the selection override mods? */ if (type == MOUSE_BINDING && !modifiers_disjoint( mods1, &conf->mouse.selection_override_modifiers)) { collision_type = COLLISION_OVERRIDE; } /* Does our binding collide with another binding? */ for (ssize_t j = i - 1; collision_type == COLLISION_NONE && j >= 0; j--) { const struct config_key_binding *binding2 = &bindings->arr[j]; xassert(binding2->action != BIND_ACTION_NONE); if (binding2->action == binding1->action && binding_aux_equal(&binding1->aux, &binding2->aux)) { continue; } const config_modifier_list_t *mods2 = &binding2->modifiers; bool mods_equal = modifiers_equal(mods1, mods2); bool sym_equal; switch (type) { case KEY_BINDING: sym_equal = binding1->k.sym == binding2->k.sym; break; case MOUSE_BINDING: sym_equal = (binding1->m.button == binding2->m.button && binding1->m.count == binding2->m.count); break; default: BUG("unhandled key binding type"); } if (!mods_equal || !sym_equal) continue; collision_binding = binding2; collision_type = COLLISION_BINDING; break; } if (collision_type != COLLISION_NONE) { char *modifier_names = modifiers_to_str(mods1, false); char sym_name[64]; switch (type){ case KEY_BINDING: xkb_keysym_get_name(binding1->k.sym, sym_name, sizeof(sym_name)); break; case MOUSE_BINDING: { const char *button_name = mouse_button_code_to_name(binding1->m.button); if (binding1->m.count > 1) { snprintf(sym_name, sizeof(sym_name), "%s-%d", button_name, binding1->m.count); } else strcpy(sym_name, button_name); break; } } switch (collision_type) { case COLLISION_NONE: break; case COLLISION_BINDING: { bool has_pipe = collision_binding->aux.type == BINDING_AUX_PIPE; LOG_AND_NOTIFY_ERR( "%s:%d: [%s].%s: %s%s already mapped to '%s%s%s%s'", binding1->path, binding1->lineno, section_name, action_map[binding1->action], modifier_names, sym_name, action_map[collision_binding->action], has_pipe ? " [" : "", has_pipe ? collision_binding->aux.pipe.args[0] : "", has_pipe ? "]" : ""); ret = false; break; } case COLLISION_OVERRIDE: { char *override_names = modifiers_to_str( &conf->mouse.selection_override_modifiers, true); if (override_names[0] != '\0') override_names[strlen(override_names) - 1] = '\0'; LOG_AND_NOTIFY_ERR( "%s:%d: [%s].%s: %s%s: " "modifiers conflict with 'selection-override-modifiers=%s'", binding1->path != NULL ? binding1->path : "(default)", binding1->lineno, section_name, action_map[binding1->action], modifier_names, sym_name, override_names); ret = false; free(override_names); break; } } free(modifier_names); if (binding1->aux.master_copy && i + 1 < bindings->count) { struct config_key_binding *next = &bindings->arr[i + 1]; if (next->action == binding1->action && binding_aux_equal(&binding1->aux, &next->aux)) { /* Transfer ownership to next binding */ next->aux.master_copy = true; binding1->aux.master_copy = false; } } free_key_binding(binding1); /* Remove the most recent binding */ size_t move_count = bindings->count - (i + 1); memmove(&bindings->arr[i], &bindings->arr[i + 1], move_count * sizeof(bindings->arr[0])); bindings->count--; i--; } } return ret; } static bool parse_section_mouse_bindings(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; const char *value = ctx->value; if (streq(key, "selection-override-modifiers")) { parse_modifiers( ctx->value, strlen(value), &conf->mouse.selection_override_modifiers); return true; } struct binding_aux aux; ssize_t pipe_remove_len = pipe_argv_from_value(ctx, &aux.pipe); if (pipe_remove_len < 0) return false; aux.type = pipe_remove_len == 0 ? BINDING_AUX_NONE : BINDING_AUX_PIPE; aux.master_copy = true; for (enum bind_action_normal action = 0; action < BIND_ACTION_COUNT; action++) { if (binding_action_map[action] == NULL) continue; if (!streq(key, binding_action_map[action])) continue; if (!value_to_key_combos( ctx, action, &aux, &conf->bindings.mouse, MOUSE_BINDING)) { free_binding_aux(&aux); return false; } return true; } LOG_CONTEXTUAL_ERR("not a valid option: %s", key); free_binding_aux(&aux); return false; } static bool parse_section_text_bindings(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; const size_t key_len = strlen(key); uint8_t *data = xmalloc(key_len + 1); size_t data_len = 0; bool esc = false; for (size_t i = 0; i < key_len; i++) { if (key[i] == '\\') { if (i + 1 >= key_len) { ctx->value = ""; LOG_CONTEXTUAL_ERR("trailing backslash"); goto err; } esc = true; } else if (esc) { if (key[i] != 'x') { ctx->value = ""; LOG_CONTEXTUAL_ERR("invalid escaped character: %c", key[i]); goto err; } if (i + 2 >= key_len) { ctx->value = ""; LOG_CONTEXTUAL_ERR("\\x sequence too short"); goto err; } const uint8_t nib1 = hex2nibble(key[i + 1]); const uint8_t nib2 = hex2nibble(key[i + 2]); if (nib1 >= HEX_DIGIT_INVALID || nib2 >= HEX_DIGIT_INVALID) { ctx->value = ""; LOG_CONTEXTUAL_ERR("invalid \\x sequence: \\x%c%c", key[i + 1], key[i + 2]); goto err; } data[data_len++] = nib1 << 4 | nib2; esc = false; i += 2; } else data[data_len++] = key[i]; } struct binding_aux aux = { .type = BINDING_AUX_TEXT, .text = { .data = data, /* data is now owned by value_to_key_combos() */ .len = data_len, }, }; if (!value_to_key_combos(ctx, BIND_ACTION_TEXT_BINDING, &aux, &conf->bindings.key, KEY_BINDING)) { /* Do *not* free(data) - it is handled by value_to_key_combos() */ return false; } return true; err: free(data); return false; } static bool parse_section_environment(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; /* Check for pre-existing env variable */ tll_foreach(conf->env_vars, it) { if (streq(it->item.name, key)) return value_to_str(ctx, &it->item.value); } /* * No pre-existing variable - allocate a new one */ char *value = NULL; if (!value_to_str(ctx, &value)) return false; tll_push_back(conf->env_vars, ((struct env_var){xstrdup(key), value})); return true; } static bool parse_section_tweak(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; if (streq(key, "scaling-filter")) { static const char *filters[] = { [FCFT_SCALING_FILTER_NONE] = "none", [FCFT_SCALING_FILTER_NEAREST] = "nearest", [FCFT_SCALING_FILTER_BILINEAR] = "bilinear", [FCFT_SCALING_FILTER_IMPULSE] = "impulse", [FCFT_SCALING_FILTER_BOX] = "box", [FCFT_SCALING_FILTER_LINEAR] = "linear", [FCFT_SCALING_FILTER_CUBIC] = "cubic", [FCFT_SCALING_FILTER_GAUSSIAN] = "gaussian", [FCFT_SCALING_FILTER_LANCZOS2] = "lanczos2", [FCFT_SCALING_FILTER_LANCZOS3] = "lanczos3", [FCFT_SCALING_FILTER_LANCZOS3_STRETCHED] = "lanczos3-stretched", NULL, }; _Static_assert(sizeof(conf->tweak.fcft_filter) == sizeof(int), "enum is not 32-bit"); return value_to_enum(ctx, filters, (int *)&conf->tweak.fcft_filter); } else if (streq(key, "overflowing-glyphs")) return value_to_bool(ctx, &conf->tweak.overflowing_glyphs); else if (streq(key, "damage-whole-window")) return value_to_bool(ctx, &conf->tweak.damage_whole_window); else if (streq(key, "grapheme-shaping")) { if (!value_to_bool(ctx, &conf->tweak.grapheme_shaping)) return false; #if !defined(FOOT_GRAPHEME_CLUSTERING) if (conf->tweak.grapheme_shaping) { LOG_CONTEXTUAL_WARN( "foot was not compiled with support for grapheme shaping"); conf->tweak.grapheme_shaping = false; } #endif if (conf->tweak.grapheme_shaping && !conf->can_shape_grapheme) { LOG_WARN( "fcft was not compiled with support for grapheme shaping"); /* Keep it enabled though - this will cause us to do * grapheme-clustering at least */ } return true; } else if (streq(key, "grapheme-width-method")) { _Static_assert(sizeof(conf->tweak.grapheme_width_method) == sizeof(int), "enum is not 32-bit"); return value_to_enum( ctx, (const char *[]){"wcswidth", "double-width", "max", NULL}, (int *)&conf->tweak.grapheme_width_method); } else if (streq(key, "render-timer")) { _Static_assert(sizeof(conf->tweak.render_timer) == sizeof(int), "enum is not 32-bit"); return value_to_enum( ctx, (const char *[]){"none", "osd", "log", "both", NULL}, (int *)&conf->tweak.render_timer); } else if (streq(key, "delayed-render-lower")) { uint32_t ns; if (!value_to_uint32(ctx, 10, &ns)) return false; if (ns > 16666666) { LOG_CONTEXTUAL_ERR("timeout must not exceed 16ms"); return false; } conf->tweak.delayed_render_lower_ns = ns; return true; } else if (streq(key, "delayed-render-upper")) { uint32_t ns; if (!value_to_uint32(ctx, 10, &ns)) return false; if (ns > 16666666) { LOG_CONTEXTUAL_ERR("timeout must not exceed 16ms"); return false; } conf->tweak.delayed_render_upper_ns = ns; return true; } else if (streq(key, "max-shm-pool-size-mb")) { uint32_t mb; if (!value_to_uint32(ctx, 10, &mb)) return false; conf->tweak.max_shm_pool_size = min((int32_t)mb * 1024 * 1024, INT32_MAX); return true; } else if (streq(key, "box-drawing-base-thickness")) return value_to_float(ctx, &conf->tweak.box_drawing_base_thickness); else if (streq(key, "box-drawing-solid-shades")) return value_to_bool(ctx, &conf->tweak.box_drawing_solid_shades); else if (streq(key, "font-monospace-warn")) return value_to_bool(ctx, &conf->tweak.font_monospace_warn); else if (streq(key, "sixel")) return value_to_bool(ctx, &conf->tweak.sixel); else if (streq(key, "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"); return value_to_enum( ctx, (const char *[]){"8-bit", "10-bit", NULL}, (int *)&conf->tweak.surface_bit_depth); } else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; } } static bool parse_section_touch(struct context *ctx) { struct config *conf = ctx->conf; const char *key = ctx->key; if (streq(key, "long-press-delay")) return value_to_uint32(ctx, 10, &conf->touch.long_press_delay); else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; } } static bool parse_key_value(char *kv, char **section, const char **key, const char **value) { bool section_is_needed = section != NULL; /* Strip leading whitespace */ while (isspace(kv[0])) ++kv; if (section_is_needed) *section = "main"; if (kv[0] == '=') return false; *key = kv; *value = NULL; size_t kvlen = strlen(kv); /* Strip trailing whitespace */ while (isspace(kv[kvlen - 1])) kvlen--; kv[kvlen] = '\0'; for (size_t i = 0; i < kvlen; ++i) { if (kv[i] == '.' && section_is_needed) { section_is_needed = false; *section = kv; kv[i] = '\0'; if (i == kvlen - 1 || kv[i + 1] == '=') { *key = NULL; return false; } *key = &kv[i + 1]; } else if (kv[i] == '=') { kv[i] = '\0'; if (i != kvlen - 1) *value = &kv[i + 1]; break; } } if (*value == NULL) return false; /* Strip trailing whitespace from key (leading stripped earlier) */ { xassert(!isspace(*key[0])); char *end = (char *)*key + strlen(*key) - 1; while (isspace(end[0])) end--; end[1] = '\0'; } /* Strip leading whitespace from value (trailing stripped earlier) */ while (isspace(*value[0])) ++*value; return true; } enum section { SECTION_MAIN, SECTION_SECURITY, SECTION_BELL, SECTION_DESKTOP_NOTIFICATIONS, SECTION_SCROLLBACK, SECTION_URL, SECTION_REGEX, SECTION_COLORS, SECTION_CURSOR, SECTION_MOUSE, SECTION_CSD, SECTION_KEY_BINDINGS, SECTION_SEARCH_BINDINGS, SECTION_URL_BINDINGS, SECTION_MOUSE_BINDINGS, SECTION_TEXT_BINDINGS, SECTION_ENVIRONMENT, SECTION_TWEAK, SECTION_TOUCH, SECTION_COUNT, }; /* Function pointer, called for each key/value line */ typedef bool (*parser_fun_t)(struct context *ctx); static const struct { parser_fun_t fun; const char *name; bool allow_colon_suffix; } section_info[] = { [SECTION_MAIN] = {&parse_section_main, "main"}, [SECTION_SECURITY] = {&parse_section_security, "security"}, [SECTION_BELL] = {&parse_section_bell, "bell"}, [SECTION_DESKTOP_NOTIFICATIONS] = {&parse_section_desktop_notifications, "desktop-notifications"}, [SECTION_SCROLLBACK] = {&parse_section_scrollback, "scrollback"}, [SECTION_URL] = {&parse_section_url, "url"}, [SECTION_REGEX] = {&parse_section_regex, "regex", true}, [SECTION_COLORS] = {&parse_section_colors, "colors"}, [SECTION_CURSOR] = {&parse_section_cursor, "cursor"}, [SECTION_MOUSE] = {&parse_section_mouse, "mouse"}, [SECTION_CSD] = {&parse_section_csd, "csd"}, [SECTION_KEY_BINDINGS] = {&parse_section_key_bindings, "key-bindings"}, [SECTION_SEARCH_BINDINGS] = {&parse_section_search_bindings, "search-bindings"}, [SECTION_URL_BINDINGS] = {&parse_section_url_bindings, "url-bindings"}, [SECTION_MOUSE_BINDINGS] = {&parse_section_mouse_bindings, "mouse-bindings"}, [SECTION_TEXT_BINDINGS] = {&parse_section_text_bindings, "text-bindings"}, [SECTION_ENVIRONMENT] = {&parse_section_environment, "environment"}, [SECTION_TWEAK] = {&parse_section_tweak, "tweak"}, [SECTION_TOUCH] = {&parse_section_touch, "touch"}, }; static_assert(ALEN(section_info) == SECTION_COUNT, "section info array size mismatch"); static enum section str_to_section(char *str, char **suffix) { *suffix = NULL; for (enum section section = SECTION_MAIN; section < SECTION_COUNT; ++section) { const char *name = section_info[section].name; if (streq(str, name)) return section; else if (section_info[section].allow_colon_suffix) { const size_t str_len = strlen(str); const size_t name_len = strlen(name); /* At least "section:" chars? */ if (str_len > name_len + 1) { if (strncmp(str, name, name_len) == 0 && str[name_len] == ':') { str[name_len] = '\0'; *suffix = &str[name_len + 1]; return section; } } } } return SECTION_COUNT; } static bool parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_are_fatal) { enum section section = SECTION_MAIN; char *_line = NULL; size_t count = 0; bool ret = true; #define error_or_continue() \ { \ if (errors_are_fatal) { \ ret = false; \ goto done; \ } else \ continue; \ } char *section_name = xstrdup("main"); char *section_suffix = NULL; struct context context = { .conf = conf, .section = section_name, .section_suffix = section_suffix, .path = path, .lineno = 0, .errors_are_fatal = errors_are_fatal, }; struct context *ctx = &context; /* For LOG_AND_*() */ errno = 0; ssize_t len; while ((len = getline(&_line, &count, f)) != -1) { context.key = NULL; context.value = NULL; context.lineno++; char *line = _line; /* Strip leading whitespace */ while (isspace(line[0])) { line++; len--; } /* Empty line, or comment */ if (line[0] == '\0' || line[0] == '#') continue; /* Strip the trailing newline - may be absent on the last line */ if (line[len - 1] == '\n') line[--len] = '\0'; /* Split up into key/value pair + trailing comment separated by blank */ char *key_value = line; char *kv_trailing = &line[len - 1]; char *comment = &line[1]; while (comment[1] != '\0') { if (isblank(comment[0]) && comment[1] == '#') { comment[1] = '\0'; /* Terminate key/value pair */ kv_trailing = comment++; break; } comment++; } comment++; /* Strip trailing whitespace */ while (isspace(kv_trailing[0])) kv_trailing--; kv_trailing[1] = '\0'; /* Check for new section */ if (key_value[0] == '[') { key_value++; if (key_value[0] == ']') { LOG_CONTEXTUAL_ERR("empty section name"); section = SECTION_COUNT; error_or_continue(); } char *end = strchr(key_value, ']'); if (end == NULL) { context.section = key_value; LOG_CONTEXTUAL_ERR("syntax error: no closing ']'"); context.section = section_name; section = SECTION_COUNT; error_or_continue(); } end[0] = '\0'; if (end[1] != '\0') { context.section = key_value; LOG_CONTEXTUAL_ERR("section declaration contains trailing " "characters"); context.section = section_name; section = SECTION_COUNT; error_or_continue(); } char *maybe_section_suffix; section = str_to_section(key_value, &maybe_section_suffix); if (section == SECTION_COUNT) { context.section = key_value; LOG_CONTEXTUAL_ERR("invalid section name: %s", key_value); context.section = section_name; error_or_continue(); } free(section_name); free(section_suffix); section_name = xstrdup(key_value); section_suffix = maybe_section_suffix != NULL ? xstrdup(maybe_section_suffix) : NULL; context.section = section_name; context.section_suffix = section_suffix; /* Process next line */ continue; } if (section >= SECTION_COUNT) { /* Last section name was invalid; ignore all keys in it */ continue; } if (!parse_key_value(key_value, NULL, &context.key, &context.value)) { LOG_CONTEXTUAL_ERR("syntax error: key/value pair has no %s", context.key == NULL ? "key" : "value"); error_or_continue(); } LOG_DBG("section=%s, key='%s', value='%s', comment='%s'", section_info[section].name, context.key, context.value, comment); xassert(section >= 0 && section < SECTION_COUNT); parser_fun_t section_parser = section_info[section].fun; xassert(section_parser != NULL); if (!section_parser(ctx)) error_or_continue(); /* For next iteration of getline() */ errno = 0; } if (errno != 0) { LOG_AND_NOTIFY_ERRNO("failed to read from configuration"); if (errors_are_fatal) ret = false; } done: free(section_name); free(section_suffix); free(_line); return ret; } static char * get_server_socket_path(void) { const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); if (xdg_runtime == NULL) return xstrdup("/tmp/foot.sock"); const char *wayland_display = getenv("WAYLAND_DISPLAY"); if (wayland_display == NULL) { return xstrjoin(xdg_runtime, "/foot.sock"); } return xasprintf("%s/foot-%s.sock", xdg_runtime, wayland_display); } static config_modifier_list_t m(const char *text) { config_modifier_list_t ret = tll_init(); parse_modifiers(text, strlen(text), &ret); return ret; } static void add_default_key_bindings(struct config *conf) { const struct config_key_binding bindings[] = { {BIND_ACTION_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Prior}}}, {BIND_ACTION_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Prior}}}, {BIND_ACTION_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Next}}}, {BIND_ACTION_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Next}}}, {BIND_ACTION_CLIPBOARD_COPY, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_c}}}, {BIND_ACTION_CLIPBOARD_COPY, m("none"), {{XKB_KEY_XF86Copy}}}, {BIND_ACTION_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_v}}}, {BIND_ACTION_CLIPBOARD_PASTE, m("none"), {{XKB_KEY_XF86Paste}}}, {BIND_ACTION_PRIMARY_PASTE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Insert}}}, {BIND_ACTION_SEARCH_START, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_r}}}, {BIND_ACTION_FONT_SIZE_UP, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_plus}}}, {BIND_ACTION_FONT_SIZE_UP, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_equal}}}, {BIND_ACTION_FONT_SIZE_UP, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_Add}}}, {BIND_ACTION_FONT_SIZE_DOWN, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_minus}}}, {BIND_ACTION_FONT_SIZE_DOWN, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_Subtract}}}, {BIND_ACTION_FONT_SIZE_RESET, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_0}}}, {BIND_ACTION_FONT_SIZE_RESET, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_0}}}, {BIND_ACTION_SPAWN_TERMINAL, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_n}}}, {BIND_ACTION_SHOW_URLS_LAUNCH, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_o}}}, {BIND_ACTION_UNICODE_INPUT, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_u}}}, {BIND_ACTION_PROMPT_PREV, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_z}}}, {BIND_ACTION_PROMPT_NEXT, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_x}}}, }; conf->bindings.key.count = ALEN(bindings); conf->bindings.key.arr = xmemdup(bindings, sizeof(bindings)); } static void add_default_search_bindings(struct config *conf) { const struct config_key_binding bindings[] = { {BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Prior}}}, {BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Prior}}}, {BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Next}}}, {BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Next}}}, {BIND_ACTION_SEARCH_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_c}}}, {BIND_ACTION_SEARCH_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_g}}}, {BIND_ACTION_SEARCH_CANCEL, m("none"), {{XKB_KEY_Escape}}}, {BIND_ACTION_SEARCH_COMMIT, m("none"), {{XKB_KEY_Return}}}, {BIND_ACTION_SEARCH_COMMIT, m("none"), {{XKB_KEY_KP_Enter}}}, {BIND_ACTION_SEARCH_FIND_PREV, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_r}}}, {BIND_ACTION_SEARCH_FIND_NEXT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_s}}}, {BIND_ACTION_SEARCH_EDIT_LEFT, m("none"), {{XKB_KEY_Left}}}, {BIND_ACTION_SEARCH_EDIT_LEFT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_b}}}, {BIND_ACTION_SEARCH_EDIT_LEFT_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Left}}}, {BIND_ACTION_SEARCH_EDIT_LEFT_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_b}}}, {BIND_ACTION_SEARCH_EDIT_RIGHT, m("none"), {{XKB_KEY_Right}}}, {BIND_ACTION_SEARCH_EDIT_RIGHT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_f}}}, {BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Right}}}, {BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_f}}}, {BIND_ACTION_SEARCH_EDIT_HOME, m("none"), {{XKB_KEY_Home}}}, {BIND_ACTION_SEARCH_EDIT_HOME, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_a}}}, {BIND_ACTION_SEARCH_EDIT_END, m("none"), {{XKB_KEY_End}}}, {BIND_ACTION_SEARCH_EDIT_END, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_e}}}, {BIND_ACTION_SEARCH_DELETE_PREV, m("none"), {{XKB_KEY_BackSpace}}}, {BIND_ACTION_SEARCH_DELETE_PREV_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_BackSpace}}}, {BIND_ACTION_SEARCH_DELETE_PREV_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_BackSpace}}}, {BIND_ACTION_SEARCH_DELETE_NEXT, m("none"), {{XKB_KEY_Delete}}}, {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Delete}}}, {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_d}}}, {BIND_ACTION_SEARCH_DELETE_TO_START, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_u}}}, {BIND_ACTION_SEARCH_DELETE_TO_END, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_k}}}, {BIND_ACTION_SEARCH_EXTEND_CHAR, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Right}}}, {BIND_ACTION_SEARCH_EXTEND_WORD, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Right}}}, {BIND_ACTION_SEARCH_EXTEND_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_w}}}, {BIND_ACTION_SEARCH_EXTEND_WORD_WS, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_w}}}, {BIND_ACTION_SEARCH_EXTEND_LINE_DOWN, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Down}}}, {BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Left}}}, {BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Left}}}, {BIND_ACTION_SEARCH_EXTEND_LINE_UP, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Up}}}, {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_v}}}, {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_v}}}, {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_y}}}, {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m("none"), {{XKB_KEY_XF86Paste}}}, {BIND_ACTION_SEARCH_PRIMARY_PASTE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Insert}}}, }; conf->bindings.search.count = ALEN(bindings); conf->bindings.search.arr = xmemdup(bindings, sizeof(bindings)); } static void add_default_url_bindings(struct config *conf) { const struct config_key_binding bindings[] = { {BIND_ACTION_URL_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_c}}}, {BIND_ACTION_URL_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_g}}}, {BIND_ACTION_URL_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_d}}}, {BIND_ACTION_URL_CANCEL, m("none"), {{XKB_KEY_Escape}}}, {BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL, m("none"), {{XKB_KEY_t}}}, }; conf->bindings.url.count = ALEN(bindings); conf->bindings.url.arr = xmemdup(bindings, sizeof(bindings)); } static void add_default_mouse_bindings(struct config *conf) { const struct config_key_binding bindings[] = { {BIND_ACTION_SCROLLBACK_UP_MOUSE, m("none"), {.m = {BTN_WHEEL_BACK, 1}}}, {BIND_ACTION_SCROLLBACK_DOWN_MOUSE, m("none"), {.m = {BTN_WHEEL_FORWARD, 1}}}, {BIND_ACTION_PRIMARY_PASTE, m("none"), {.m = {BTN_MIDDLE, 1}}}, {BIND_ACTION_SELECT_BEGIN, m("none"), {.m = {BTN_LEFT, 1}}}, {BIND_ACTION_SELECT_BEGIN_BLOCK, m(XKB_MOD_NAME_CTRL), {.m = {BTN_LEFT, 1}}}, {BIND_ACTION_SELECT_EXTEND, m("none"), {.m = {BTN_RIGHT, 1}}}, {BIND_ACTION_SELECT_EXTEND_CHAR_WISE, m(XKB_MOD_NAME_CTRL), {.m = {BTN_RIGHT, 1}}}, {BIND_ACTION_SELECT_WORD, m("none"), {.m = {BTN_LEFT, 2}}}, {BIND_ACTION_SELECT_WORD_WS, m(XKB_MOD_NAME_CTRL), {.m = {BTN_LEFT, 2}}}, {BIND_ACTION_SELECT_QUOTE, m("none"), {.m = {BTN_LEFT, 3}}}, {BIND_ACTION_SELECT_ROW, m("none"), {.m = {BTN_LEFT, 4}}}, {BIND_ACTION_FONT_SIZE_UP, m("Control"), {.m = {BTN_WHEEL_BACK, 1}}}, {BIND_ACTION_FONT_SIZE_DOWN, m("Control"), {.m = {BTN_WHEEL_FORWARD, 1}}}, }; conf->bindings.mouse.count = ALEN(bindings); conf->bindings.mouse.arr = xmemdup(bindings, sizeof(bindings)); } static void NOINLINE config_font_list_clone(struct config_font_list *dst, const struct config_font_list *src) { dst->count = src->count; dst->arr = xmalloc(dst->count * sizeof(dst->arr[0])); for (size_t j = 0; j < dst->count; j++) { dst->arr[j].pt_size = src->arr[j].pt_size; dst->arr[j].px_size = src->arr[j].px_size; dst->arr[j].pattern = xstrdup(src->arr[j].pattern); } } bool config_load(struct config *conf, const char *conf_path, user_notifications_t *initial_user_notifications, config_override_t *overrides, bool errors_are_fatal, bool as_server) { bool ret = true; enum fcft_capabilities fcft_caps = fcft_capabilities(); *conf = (struct config) { .term = xstrdup(FOOT_DEFAULT_TERM), .shell = get_shell(), .title = xstrdup("foot"), .app_id = (as_server ? xstrdup("footclient") : xstrdup("foot")), .word_delimiters = xc32dup(U",│`|:\"'()[]{}<>"), .size = { .type = CONF_SIZE_PX, .width = 700, .height = 500, }, .pad_x = 0, .pad_y = 0, .resize_by_cells = true, .resize_keep_grid = true, .resize_delay_ms = 100, .bold_in_bright = { .enabled = false, .palette_based = false, .amount = 1.3, }, .startup_mode = STARTUP_WINDOWED, .fonts = {{0}}, .font_size_adjustment = {.percent = 0., .pt_or_px = {.pt = 0.5, .px = 0}}, .line_height = {.pt = 0, .px = -1}, .letter_spacing = {.pt = 0, .px = 0}, .horizontal_letter_offset = {.pt = 0, .px = 0}, .vertical_letter_offset = {.pt = 0, .px = 0}, .use_custom_underline_offset = false, .box_drawings_uses_font_glyphs = false, .underline_thickness = {.pt = 0., .px = -1}, .strikeout_thickness = {.pt = 0., .px = -1}, .dpi_aware = false, .gamma_correct = GAMMA_CORRECT_AUTO, .security = { .osc52 = OSC52_ENABLED, }, .bell = { .urgent = false, .notify = false, .flash = false, .system_bell = true, .command = { .argv = {.args = NULL}, }, .command_focused = false, }, .url = { .label_letters = xc32dup(U"sadfjklewcmpgh"), .osc8_underline = OSC8_UNDERLINE_URL_MODE, }, .custom_regexes = tll_init(), .can_shape_grapheme = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING, .scrollback = { .lines = 1000, .indicator = { .position = SCROLLBACK_INDICATOR_POSITION_RELATIVE, .format = SCROLLBACK_INDICATOR_FORMAT_TEXT, .text = xc32dup(U""), }, .multiplier = 3., }, .colors = { .fg = default_foreground, .bg = default_background, .flash = 0x7f7f00, .flash_alpha = 0x7fff, .alpha = 0xffff, .selection_fg = 0x80000000, /* Use default bg */ .selection_bg = 0x80000000, /* Use default fg */ .use_custom = { .selection = false, .jump_label = false, .scrollback_indicator = false, .url = false, }, }, .cursor = { .style = CURSOR_BLOCK, .unfocused_style = CURSOR_UNFOCUSED_HOLLOW, .blink = { .enabled = false, .rate_ms = 500, }, .color = { .text = 0, .cursor = 0, }, .beam_thickness = {.pt = 1.5}, .underline_thickness = {.pt = 0., .px = -1}, }, .mouse = { .hide_when_typing = false, .alternate_scroll_mode = true, .selection_override_modifiers = tll_init(), }, .csd = { .preferred = CONF_CSD_PREFER_SERVER, .font = {0}, .hide_when_maximized = false, .double_click_to_maximize = true, .title_height = 26, .border_width = 5, .border_width_visible = 0, .button_width = 26, }, .render_worker_count = sysconf(_SC_NPROCESSORS_ONLN), .server_socket_path = get_server_socket_path(), .presentation_timings = false, .selection_target = SELECTION_TARGET_PRIMARY, .hold_at_exit = false, .desktop_notifications = { .command = { .argv = {.args = NULL}, }, .command_action_arg = { .argv = {.args = NULL}, }, .close = { .argv = {.args = NULL}, }, .inhibit_when_focused = true, }, .tweak = { .fcft_filter = FCFT_SCALING_FILTER_LANCZOS3, .overflowing_glyphs = true, #if defined(FOOT_GRAPHEME_CLUSTERING) && FOOT_GRAPHEME_CLUSTERING .grapheme_shaping = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING, #endif .grapheme_width_method = GRAPHEME_WIDTH_DOUBLE, .delayed_render_lower_ns = 500000, /* 0.5ms */ .delayed_render_upper_ns = 16666666 / 2, /* half a frame period (60Hz) */ .max_shm_pool_size = 512 * 1024 * 1024, .render_timer = RENDER_TIMER_NONE, .damage_whole_window = false, .box_drawing_base_thickness = 0.04, .box_drawing_solid_shades = true, .font_monospace_warn = true, .sixel = true, .surface_bit_depth = 8, }, .touch = { .long_press_delay = 400, }, .env_vars = tll_init(), #if defined(UTMP_DEFAULT_HELPER_PATH) .utmp_helper_path = ((strlen(UTMP_DEFAULT_HELPER_PATH) > 0 && access(UTMP_DEFAULT_HELPER_PATH, X_OK) == 0) ? xstrdup(UTMP_DEFAULT_HELPER_PATH) : NULL), #endif .notifications = tll_init(), }; memcpy(conf->colors.table, default_color_table, sizeof(default_color_table)); memcpy(conf->colors.sixel, default_sixel_colors, sizeof(default_sixel_colors)); 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); { /* * Based on https://gist.github.com/gruber/249502, but modified: * - Do not allow {} at all * - Do allow matched [] */ const char *url_regex_string = "(" "(" "[a-z][[:alnum:]-]+:" // protocol "(" "/{1,3}|[a-z0-9%]" // slashes (what's the OR part for?) ")" "|" "www[:digit:]{0,3}[.]" //"|" //"[a-z0-9.\\-]+[.][a-z]{2,4}/" /* "looks like domain name followed by a slash" - remove? */ ")" "(" "[^[:space:](){}<>]+" "|" "\\(([^[:space:](){}<>]+|(\\([^[:space:](){}<>]+\\)))*\\)" "|" "\\[([^]\\[[:space:](){}<>]+|(\\[[^]\\[[:space:](){}<>]+\\]))*\\]" ")+" "(" "\\(([^[:space:](){}<>]+|(\\([^[:space:](){}<>]+\\)))*\\)" "|" "\\[([^]\\[[:space:](){}<>]+|(\\[[^]\\[[:space:](){}<>]+\\]))*\\]" "|" "[^]\\[[:space:]`!(){};:'\".,<>?«»“”‘’]" ")" ")" ; int r = regcomp(&conf->url.preg, url_regex_string, REG_EXTENDED); xassert(r == 0); conf->url.regex = xstrdup(url_regex_string); xassert(conf->url.preg.re_nsub >= 1); } tll_foreach(*initial_user_notifications, it) { tll_push_back(conf->notifications, it->item); tll_remove(*initial_user_notifications, it); } add_default_key_bindings(conf); add_default_search_bindings(conf); add_default_url_bindings(conf); add_default_mouse_bindings(conf); struct config_file conf_file = {.path = NULL, .fd = -1}; if (conf_path != NULL) { int fd = open(conf_path, O_RDONLY); if (fd < 0) { LOG_AND_NOTIFY_ERRNO("%s: failed to open", conf_path); ret = !errors_are_fatal; } else { conf_file.path = xstrdup(conf_path); conf_file.fd = fd; } } else { conf_file = open_config(); if (conf_file.fd < 0) { LOG_WARN("no configuration found, using defaults"); ret = !errors_are_fatal; } } if (conf_file.path && conf_file.fd >= 0) { LOG_INFO("loading configuration from %s", conf_file.path); FILE *f = fdopen(conf_file.fd, "r"); if (f == NULL) { LOG_AND_NOTIFY_ERRNO("%s: failed to open", conf_file.path); ret = !errors_are_fatal; } else { if (!parse_config_file(f, conf, conf_file.path, errors_are_fatal)) ret = !errors_are_fatal; fclose(f); conf_file.fd = -1; } } if (!config_override_apply(conf, overrides, errors_are_fatal)) ret = !errors_are_fatal; conf->colors.use_custom.selection = conf->colors.selection_fg >> 24 == 0 && conf->colors.selection_bg >> 24 == 0; if (ret && conf->fonts[0].count == 0) { struct config_font font; if (!config_font_parse("monospace", &font)) { LOG_ERR("failed to load font 'monospace' - no fonts installed?"); ret = false; } else { conf->fonts[0].count = 1; conf->fonts[0].arr = xmalloc(sizeof(font)); conf->fonts[0].arr[0] = font; } } if (ret && conf->csd.font.count == 0) config_font_list_clone(&conf->csd.font, &conf->fonts[0]); #if defined(_DEBUG) for (size_t i = 0; i < conf->bindings.key.count; i++) xassert(conf->bindings.key.arr[i].action != BIND_ACTION_NONE); for (size_t i = 0; i < conf->bindings.search.count; i++) xassert(conf->bindings.search.arr[i].action != BIND_ACTION_SEARCH_NONE); for (size_t i = 0; i < conf->bindings.url.count; i++) xassert(conf->bindings.url.arr[i].action != BIND_ACTION_URL_NONE); #endif free(conf_file.path); if (conf_file.fd >= 0) close(conf_file.fd); return ret; } bool config_override_apply(struct config *conf, config_override_t *overrides, bool errors_are_fatal) { char *section_name = NULL; struct context context = { .conf = conf, .path = "override", .lineno = 0, .errors_are_fatal = errors_are_fatal, }; struct context *ctx = &context; tll_foreach(*overrides, it) { context.lineno++; if (!parse_key_value(it->item, §ion_name, &context.key, &context.value)) { LOG_CONTEXTUAL_ERR("syntax error: key/value pair has no %s", context.key == NULL ? "key" : "value"); if (errors_are_fatal) return false; continue; } if (section_name[0] == '\0') { LOG_CONTEXTUAL_ERR("empty section name"); if (errors_are_fatal) return false; continue; } char *maybe_section_suffix = NULL; enum section section = str_to_section(section_name, &maybe_section_suffix); context.section = section_name; context.section_suffix = maybe_section_suffix; if (section == SECTION_COUNT) { LOG_CONTEXTUAL_ERR("invalid section name: %s", section_name); if (errors_are_fatal) return false; continue; } parser_fun_t section_parser = section_info[section].fun; xassert(section_parser != NULL); if (!section_parser(ctx)) { if (errors_are_fatal) return false; continue; } } conf->csd.border_width = max( min_csd_border_width, conf->csd.border_width_visible); return resolve_key_binding_collisions( conf, section_info[SECTION_KEY_BINDINGS].name, binding_action_map, &conf->bindings.key, KEY_BINDING) && resolve_key_binding_collisions( conf, section_info[SECTION_SEARCH_BINDINGS].name, search_binding_action_map, &conf->bindings.search, KEY_BINDING) && resolve_key_binding_collisions( conf, section_info[SECTION_URL_BINDINGS].name, url_binding_action_map, &conf->bindings.url, KEY_BINDING) && resolve_key_binding_collisions( conf, section_info[SECTION_MOUSE_BINDINGS].name, binding_action_map, &conf->bindings.mouse, MOUSE_BINDING); } static void NOINLINE key_binding_list_clone(struct config_key_binding_list *dst, const struct config_key_binding_list *src) { struct argv *last_master_argv = NULL; uint8_t *last_master_text_data = NULL; size_t last_master_text_len = 0; char *last_master_regex_name = NULL; dst->count = src->count; dst->arr = xmalloc(src->count * sizeof(dst->arr[0])); for (size_t i = 0; i < src->count; i++) { const struct config_key_binding *old = &src->arr[i]; struct config_key_binding *new = &dst->arr[i]; *new = *old; memset(&new->modifiers, 0, sizeof(new->modifiers)); tll_foreach(old->modifiers, it) tll_push_back(new->modifiers, xstrdup(it->item)); switch (old->aux.type) { case BINDING_AUX_NONE: last_master_argv = NULL; last_master_text_data = NULL; last_master_text_len = 0; break; case BINDING_AUX_PIPE: if (old->aux.master_copy) { clone_argv(&new->aux.pipe, &old->aux.pipe); last_master_argv = &new->aux.pipe; } else { xassert(last_master_argv != NULL); new->aux.pipe = *last_master_argv; } last_master_text_data = NULL; last_master_text_len = 0; break; case BINDING_AUX_TEXT: if (old->aux.master_copy) { const size_t len = old->aux.text.len; new->aux.text.len = len; new->aux.text.data = xmemdup(old->aux.text.data, len); last_master_text_len = len; last_master_text_data = new->aux.text.data; } else { xassert(last_master_text_data != NULL); new->aux.text.len = last_master_text_len; new->aux.text.data = last_master_text_data; } last_master_argv = NULL; break; case BINDING_AUX_REGEX: if (old->aux.master_copy) { new->aux.regex_name = xstrdup(old->aux.regex_name); last_master_regex_name = new->aux.regex_name; } else { xassert(last_master_regex_name != NULL); new->aux.regex_name = last_master_regex_name; } break; } } } struct config * config_clone(const struct config *old) { struct config *conf = xmalloc(sizeof(*conf)); *conf = *old; conf->term = xstrdup(old->term); conf->shell = xstrdup(old->shell); conf->title = xstrdup(old->title); conf->app_id = xstrdup(old->app_id); conf->word_delimiters = xc32dup(old->word_delimiters); conf->scrollback.indicator.text = xc32dup(old->scrollback.indicator.text); conf->server_socket_path = xstrdup(old->server_socket_path); spawn_template_clone(&conf->bell.command, &old->bell.command); spawn_template_clone(&conf->desktop_notifications.command, &old->desktop_notifications.command); spawn_template_clone(&conf->desktop_notifications.command_action_arg, &old->desktop_notifications.command_action_arg); spawn_template_clone(&conf->desktop_notifications.close, &old->desktop_notifications.close); for (size_t i = 0; i < ALEN(conf->fonts); i++) config_font_list_clone(&conf->fonts[i], &old->fonts[i]); config_font_list_clone(&conf->csd.font, &old->csd.font); conf->url.label_letters = xc32dup(old->url.label_letters); spawn_template_clone(&conf->url.launch, &old->url.launch); conf->url.regex = xstrdup(old->url.regex); regcomp(&conf->url.preg, conf->url.regex, REG_EXTENDED); memset(&conf->custom_regexes, 0, sizeof(conf->custom_regexes)); tll_foreach(old->custom_regexes, it) { const struct custom_regex *old_regex = &it->item; tll_push_back(conf->custom_regexes, ((struct custom_regex){.name = xstrdup(old_regex->name), .regex = xstrdup(old_regex->regex)})); struct custom_regex *new_regex = &tll_back(conf->custom_regexes); regcomp(&new_regex->preg, new_regex->regex, REG_EXTENDED); spawn_template_clone(&new_regex->launch, &old_regex->launch); } key_binding_list_clone(&conf->bindings.key, &old->bindings.key); key_binding_list_clone(&conf->bindings.search, &old->bindings.search); key_binding_list_clone(&conf->bindings.url, &old->bindings.url); key_binding_list_clone(&conf->bindings.mouse, &old->bindings.mouse); conf->env_vars.length = 0; conf->env_vars.head = conf->env_vars.tail = NULL; memset(&conf->mouse.selection_override_modifiers, 0, sizeof(conf->mouse.selection_override_modifiers)); tll_foreach(old->mouse.selection_override_modifiers, it) tll_push_back(conf->mouse.selection_override_modifiers, xstrdup(it->item)); tll_foreach(old->env_vars, it) { struct env_var copy = { .name = xstrdup(it->item.name), .value = xstrdup(it->item.value), }; tll_push_back(conf->env_vars, copy); } conf->utmp_helper_path = old->utmp_helper_path != NULL ? xstrdup(old->utmp_helper_path) : NULL; conf->notifications.length = 0; conf->notifications.head = conf->notifications.tail = 0; tll_foreach(old->notifications, it) { char *text = xstrdup(it->item.text); user_notification_add(&conf->notifications, it->item.kind, text); } return conf; } UNITTEST { struct config original; user_notifications_t nots = tll_init(); config_override_t overrides = tll_init(); fcft_init(FCFT_LOG_COLORIZE_NEVER, false, FCFT_LOG_CLASS_NONE); bool ret = config_load(&original, "/dev/null", ¬s, &overrides, false, false); xassert(ret); //struct config *clone = config_clone(&original); //xassert(clone != NULL); //xassert(clone != &original); config_free(&original); //config_free(clone); //free(clone); fcft_fini(); tll_free(overrides); tll_free(nots); } void config_free(struct config *conf) { free(conf->term); free(conf->shell); free(conf->title); free(conf->app_id); free(conf->word_delimiters); spawn_template_free(&conf->bell.command); free(conf->scrollback.indicator.text); spawn_template_free(&conf->desktop_notifications.command); spawn_template_free(&conf->desktop_notifications.command_action_arg); spawn_template_free(&conf->desktop_notifications.close); for (size_t i = 0; i < ALEN(conf->fonts); i++) config_font_list_destroy(&conf->fonts[i]); free(conf->server_socket_path); config_font_list_destroy(&conf->csd.font); free(conf->url.label_letters); spawn_template_free(&conf->url.launch); regfree(&conf->url.preg); free(conf->url.regex); tll_foreach(conf->custom_regexes, it) { struct custom_regex *regex = &it->item; free(regex->name); free(regex->regex); regfree(®ex->preg); spawn_template_free(®ex->launch); tll_remove(conf->custom_regexes, it); } free_key_binding_list(&conf->bindings.key); free_key_binding_list(&conf->bindings.search); free_key_binding_list(&conf->bindings.url); free_key_binding_list(&conf->bindings.mouse); tll_free_and_free(conf->mouse.selection_override_modifiers, free); tll_foreach(conf->env_vars, it) { free(it->item.name); free(it->item.value); tll_remove(conf->env_vars, it); } free(conf->utmp_helper_path); user_notifications_free(&conf->notifications); } bool config_font_parse(const char *pattern, struct config_font *font) { FcPattern *pat = FcNameParse((const FcChar8 *)pattern); if (pat == NULL) return false; /* * First look for user specified {pixel}size option * e.g. "font-name:size=12" */ double pt_size = -1.0; FcResult have_pt_size = FcPatternGetDouble(pat, FC_SIZE, 0, &pt_size); int px_size = -1; FcResult have_px_size = FcPatternGetInteger(pat, FC_PIXEL_SIZE, 0, &px_size); if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) { /* * Apply fontconfig config. Can't do that until we've first * checked for a user provided size, since we may end up with * both "size" and "pixelsize" being set, and we don't know * which one takes priority. */ FcPattern *pat_copy = FcPatternDuplicate(pat); if (pat_copy == NULL || !FcConfigSubstitute(NULL, pat_copy, FcMatchPattern)) { LOG_WARN("%s: failed to do config substitution", pattern); } else { have_pt_size = FcPatternGetDouble(pat_copy, FC_SIZE, 0, &pt_size); have_px_size = FcPatternGetInteger(pat_copy, FC_PIXEL_SIZE, 0, &px_size); } FcPatternDestroy(pat_copy); if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) pt_size = 8.0; } FcPatternRemove(pat, FC_SIZE, 0); FcPatternRemove(pat, FC_PIXEL_SIZE, 0); char *stripped_pattern = (char *)FcNameUnparse(pat); FcPatternDestroy(pat); LOG_DBG("%s: pt-size=%.2f, px-size=%d", stripped_pattern, pt_size, px_size); if (stripped_pattern == NULL) { LOG_ERR("failed to convert font pattern to string"); return false; } *font = (struct config_font){ .pattern = stripped_pattern, .pt_size = pt_size, .px_size = px_size }; return true; } void config_font_list_destroy(struct config_font_list *font_list) { for (size_t i = 0; i < font_list->count; i++) free(font_list->arr[i].pattern); free(font_list->arr); font_list->count = 0; font_list->arr = NULL; } bool check_if_font_is_monospaced(const char *pattern, user_notifications_t *notifications) { struct fcft_font *f = fcft_from_name( 1, (const char *[]){pattern}, ":size=8"); if (f == NULL) return true; static const char32_t chars[] = {U'a', U'i', U'l', U'M', U'W'}; bool is_monospaced = true; int last_width = -1; for (size_t i = 0; i < sizeof(chars) / sizeof(chars[0]); i++) { const struct fcft_glyph *g = fcft_rasterize_char_utf32( f, chars[i], FCFT_SUBPIXEL_NONE); if (g == NULL) continue; if (last_width >= 0 && g->advance.x != last_width) { const char *font_name = f->name != NULL ? f->name : pattern; LOG_WARN("%s: font does not appear to be monospace; " "check your config, or disable this warning by " "setting [tweak].font-monospace-warn=no", font_name); static const char fmt[] = "%s: font does not appear to be monospace; " "check your config, or disable this warning by " "setting \033[1m[tweak].font-monospace-warn=no\033[22m"; user_notification_add_fmt( notifications, USER_NOTIFICATION_WARNING, fmt, font_name); is_monospaced = false; break; } last_width = g->advance.x; } fcft_destroy(f); return is_monospaced; } #if 0 xkb_mod_mask_t conf_modifiers_to_mask(const struct seat *seat, const struct config_key_modifiers *modifiers) { xkb_mod_mask_t mods = 0; if (seat->kbd.mod_shift != XKB_MOD_INVALID) mods |= modifiers->shift << seat->kbd.mod_shift; if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) mods |= modifiers->ctrl << seat->kbd.mod_ctrl; if (seat->kbd.mod_alt != XKB_MOD_INVALID) mods |= modifiers->alt << seat->kbd.mod_alt; if (seat->kbd.mod_super != XKB_MOD_INVALID) mods |= modifiers->super << seat->kbd.mod_super; return mods; } #endif foot-1.21.0/config.h000066400000000000000000000234761476600145200142010ustar00rootroot00000000000000#pragma once #include #include #include #include #include #include #include #include "user-notification.h" #define DEFINE_LIST(type) \ type##_list { \ size_t count; \ type *arr; \ } /* If px != 0 then px is valid, otherwise pt is valid */ struct pt_or_px { int16_t px; float pt; }; struct font_size_adjustment { struct pt_or_px pt_or_px; float percent; }; enum cursor_style { CURSOR_BLOCK, CURSOR_UNDERLINE, CURSOR_BEAM, CURSOR_HOLLOW }; enum cursor_unfocused_style { CURSOR_UNFOCUSED_UNCHANGED, CURSOR_UNFOCUSED_HOLLOW, CURSOR_UNFOCUSED_NONE }; enum conf_size_type {CONF_SIZE_PX, CONF_SIZE_CELLS}; struct config_font { char *pattern; float pt_size; int px_size; }; DEFINE_LIST(struct config_font); #if 0 struct config_key_modifiers { bool shift; bool alt; bool ctrl; bool super; }; #endif struct argv { char **args; }; enum binding_aux_type { BINDING_AUX_NONE, BINDING_AUX_PIPE, BINDING_AUX_TEXT, BINDING_AUX_REGEX, }; struct binding_aux { enum binding_aux_type type; bool master_copy; union { struct argv pipe; struct { uint8_t *data; size_t len; } text; char *regex_name; }; }; enum key_binding_type { KEY_BINDING, MOUSE_BINDING, }; typedef tll(char *) config_modifier_list_t; struct config_key_binding { int action; /* One of the various bind_action_* enums from wayland.h */ //struct config_key_modifiers modifiers; config_modifier_list_t modifiers; union { /* Key bindings */ struct { xkb_keysym_t sym; } k; /* Mouse bindings */ struct { int button; int count; } m; }; struct binding_aux aux; /* For error messages in collision handling */ const char *path; int lineno; }; DEFINE_LIST(struct config_key_binding); typedef tll(char *) config_override_t; struct config_spawn_template { struct argv argv; }; struct env_var { char *name; char *value; }; typedef tll(struct env_var) env_var_list_t; struct custom_regex { char *name; char *regex; regex_t preg; struct config_spawn_template launch; }; struct config { char *term; char *shell; char *title; char *app_id; char32_t *word_delimiters; bool login_shell; bool locked_title; struct { enum conf_size_type type; uint32_t width; uint32_t height; } size; unsigned pad_x; unsigned pad_y; bool center; bool resize_by_cells; bool resize_keep_grid; uint16_t resize_delay_ms; struct { bool enabled; bool palette_based; float amount; } bold_in_bright; enum { STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN } startup_mode; bool dpi_aware; enum {GAMMA_CORRECT_DISABLED, GAMMA_CORRECT_ENABLED, GAMMA_CORRECT_AUTO} gamma_correct; struct config_font_list fonts[4]; struct font_size_adjustment font_size_adjustment; /* Custom font metrics (-1 = use real font metrics) */ struct pt_or_px line_height; struct pt_or_px letter_spacing; /* Adjusted letter x/y offsets */ struct pt_or_px horizontal_letter_offset; struct pt_or_px vertical_letter_offset; bool use_custom_underline_offset; struct pt_or_px underline_offset; struct pt_or_px underline_thickness; struct pt_or_px strikeout_thickness; bool box_drawings_uses_font_glyphs; bool can_shape_grapheme; struct { enum { OSC52_DISABLED, OSC52_COPY_ENABLED, OSC52_PASTE_ENABLED, OSC52_ENABLED, } osc52; } security; struct { bool urgent; bool notify; bool flash; bool system_bell; struct config_spawn_template command; bool command_focused; } bell; struct { uint32_t lines; struct { enum { SCROLLBACK_INDICATOR_POSITION_NONE, SCROLLBACK_INDICATOR_POSITION_FIXED, SCROLLBACK_INDICATOR_POSITION_RELATIVE } position; enum { SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE, SCROLLBACK_INDICATOR_FORMAT_LINENO, SCROLLBACK_INDICATOR_FORMAT_TEXT, } format; char32_t *text; } indicator; float multiplier; } scrollback; struct { char32_t *label_letters; struct config_spawn_template launch; enum { OSC8_UNDERLINE_URL_MODE, OSC8_UNDERLINE_ALWAYS, } osc8_underline; char *regex; regex_t preg; } url; tll(struct custom_regex) custom_regexes; struct { 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]; 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 { enum cursor_style style; enum cursor_unfocused_style unfocused_style; struct { bool enabled; uint32_t rate_ms; } blink; struct { uint32_t text; uint32_t cursor; } color; struct pt_or_px beam_thickness; struct pt_or_px underline_thickness; } cursor; struct { bool hide_when_typing; bool alternate_scroll_mode; //struct config_key_modifiers selection_override_modifiers; config_modifier_list_t selection_override_modifiers; } mouse; struct { /* Bindings for "normal" mode */ struct config_key_binding_list key; struct config_key_binding_list mouse; /* * Special modes */ /* While searching (not - action to *start* a search is in the * 'key' bindings above */ struct config_key_binding_list search; /* While showing URL jump labels */ struct config_key_binding_list url; } bindings; struct { enum { CONF_CSD_PREFER_NONE, CONF_CSD_PREFER_SERVER, CONF_CSD_PREFER_CLIENT } preferred; uint16_t title_height; uint16_t border_width; uint16_t border_width_visible; uint16_t button_width; bool hide_when_maximized; bool double_click_to_maximize; struct { bool title_set:1; bool buttons_set:1; bool minimize_set:1; bool maximize_set:1; bool close_set:1; bool border_set:1; uint32_t title; uint32_t buttons; uint32_t minimize; uint32_t maximize; uint32_t quit; /* 'close' collides with #define in epoll-shim */ uint32_t border; } color; struct config_font_list font; } csd; uint16_t render_worker_count; char *server_socket_path; bool presentation_timings; bool hold_at_exit; enum { SELECTION_TARGET_NONE, SELECTION_TARGET_PRIMARY, SELECTION_TARGET_CLIPBOARD, SELECTION_TARGET_BOTH } selection_target; struct { struct config_spawn_template command; struct config_spawn_template command_action_arg; struct config_spawn_template close; bool inhibit_when_focused; } desktop_notifications; env_var_list_t env_vars; char *utmp_helper_path; struct { enum fcft_scaling_filter fcft_filter; bool overflowing_glyphs; bool grapheme_shaping; enum { GRAPHEME_WIDTH_WCSWIDTH, GRAPHEME_WIDTH_DOUBLE, GRAPHEME_WIDTH_MAX, } grapheme_width_method; enum { RENDER_TIMER_NONE, RENDER_TIMER_OSD, RENDER_TIMER_LOG, RENDER_TIMER_BOTH } render_timer; bool damage_whole_window; uint32_t delayed_render_lower_ns; uint32_t delayed_render_upper_ns; off_t max_shm_pool_size; float box_drawing_base_thickness; bool box_drawing_solid_shades; bool font_monospace_warn; bool sixel; enum { SHM_8_BIT, SHM_10_BIT } surface_bit_depth; } tweak; struct { uint32_t long_press_delay; } touch; user_notifications_t notifications; }; bool config_override_apply(struct config *conf, config_override_t *overrides, bool errors_are_fatal); bool config_load( struct config *conf, const char *path, user_notifications_t *initial_user_notifications, config_override_t *overrides, bool errors_are_fatal, bool as_server); void config_free(struct config *conf); struct config *config_clone(const struct config *old); bool config_font_parse(const char *pattern, struct config_font *font); void config_font_list_destroy(struct config_font_list *font_list); #if 0 struct seat; xkb_mod_mask_t conf_modifiers_to_mask( const struct seat *seat, const struct config_key_modifiers *modifiers); #endif bool check_if_font_is_monospaced( const char *pattern, user_notifications_t *notifications); foot-1.21.0/csi.c000066400000000000000000002171041476600145200134760ustar00rootroot00000000000000#include "csi.h" #include #include #include #if defined(_DEBUG) #include #endif #include #define LOG_MODULE "csi" #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" #include "config.h" #include "debug.h" #include "grid.h" #include "selection.h" #include "sixel.h" #include "util.h" #include "version.h" #include "vt.h" #include "xmalloc.h" #include "xsnprintf.h" #define UNHANDLED() LOG_DBG("unhandled: %s", csi_as_string(term, final, -1)) #define UNHANDLED_SGR(idx) LOG_DBG("unhandled: %s", csi_as_string(term, 'm', idx)) static void sgr_reset(struct terminal *term) { term->vt.attrs = (struct attributes){0}; term->vt.underline = (struct underline_range_data){0}; term->bits_affecting_ascii_printer.underline_style = false; term->bits_affecting_ascii_printer.underline_color = false; term_update_ascii_printer(term); } static const char * csi_as_string(struct terminal *term, uint8_t final, int idx) { static char msg[1024]; int c = snprintf(msg, sizeof(msg), "CSI: "); for (size_t i = idx >= 0 ? idx : 0; i < (idx >= 0 ? idx + 1 : term->vt.params.idx); i++) { c += snprintf(&msg[c], sizeof(msg) - c, "%u", term->vt.params.v[i].value); for (size_t j = 0; j < term->vt.params.v[i].sub.idx; j++) { c += snprintf(&msg[c], sizeof(msg) - c, ":%u", term->vt.params.v[i].sub.value[j]); } c += snprintf(&msg[c], sizeof(msg) - c, "%s", i == term->vt.params.idx - 1 ? "" : ";"); } for (size_t i = 0; i < sizeof(term->vt.private); i++) { char value = (term->vt.private >> (i * 8)) & 0xff; if (value == 0) break; c += snprintf(&msg[c], sizeof(msg) - c, "%c", value); } snprintf(&msg[c], sizeof(msg) - c, "%c (%u parameters)", final, idx >= 0 ? 1 : term->vt.params.idx); return msg; } static void csi_sgr(struct terminal *term) { if (term->vt.params.idx == 0) { sgr_reset(term); return; } for (size_t i = 0; i < term->vt.params.idx; i++) { const int param = term->vt.params.v[i].value; switch (param) { case 0: sgr_reset(term); break; case 1: term->vt.attrs.bold = true; break; case 2: term->vt.attrs.dim = true; break; case 3: term->vt.attrs.italic = true; break; case 4: { term->vt.attrs.underline = true; term->vt.underline.style = UNDERLINE_SINGLE; if (unlikely(term->vt.params.v[i].sub.idx == 1)) { enum underline_style style = term->vt.params.v[i].sub.value[0]; switch (style) { default: case UNDERLINE_NONE: term->vt.attrs.underline = false; term->vt.underline.style = UNDERLINE_NONE; term->bits_affecting_ascii_printer.underline_style = false; break; case UNDERLINE_SINGLE: case UNDERLINE_DOUBLE: case UNDERLINE_CURLY: case UNDERLINE_DOTTED: case UNDERLINE_DASHED: term->vt.underline.style = style; term->bits_affecting_ascii_printer.underline_style = style > UNDERLINE_SINGLE; break; } term_update_ascii_printer(term); } break; } case 5: term->vt.attrs.blink = true; break; case 6: LOG_WARN("ignored: rapid blink"); break; case 7: term->vt.attrs.reverse = true; break; case 8: term->vt.attrs.conceal = true; break; case 9: term->vt.attrs.strikethrough = true; break; case 21: term->vt.attrs.underline = true; term->vt.underline.style = UNDERLINE_DOUBLE; term->bits_affecting_ascii_printer.underline_style = true; term_update_ascii_printer(term); break; case 22: term->vt.attrs.bold = term->vt.attrs.dim = false; break; case 23: term->vt.attrs.italic = false; break; case 24: { term->vt.attrs.underline = false; term->vt.underline.style = UNDERLINE_NONE; term->bits_affecting_ascii_printer.underline_style = false; term_update_ascii_printer(term); break; } case 25: term->vt.attrs.blink = false; break; case 26: break; /* rapid blink, ignored */ case 27: term->vt.attrs.reverse = false; break; case 28: term->vt.attrs.conceal = false; break; case 29: term->vt.attrs.strikethrough = false; break; /* Regular foreground colors */ case 30: case 31: case 32: case 33: case 34: case 35: case 36: case 37: term->vt.attrs.fg_src = COLOR_BASE16; term->vt.attrs.fg = param - 30; break; case 38: case 48: case 58: { uint32_t color; enum color_source src; /* Indexed: 38;5; */ if (term->vt.params.idx - i - 1 >= 2 && term->vt.params.v[i + 1].value == 5) { src = COLOR_BASE256; color = min(term->vt.params.v[i + 2].value, ALEN(term->colors.table) - 1); i += 2; } /* RGB: 38;2;;; */ else if (term->vt.params.idx - i - 1 >= 4 && term->vt.params.v[i + 1].value == 2) { uint8_t r = term->vt.params.v[i + 2].value; uint8_t g = term->vt.params.v[i + 3].value; uint8_t b = term->vt.params.v[i + 4].value; src = COLOR_RGB; color = r << 16 | g << 8 | b; i += 4; } /* Indexed: 38:5: */ else if (term->vt.params.v[i].sub.idx >= 2 && term->vt.params.v[i].sub.value[0] == 5) { src = COLOR_BASE256; color = min(term->vt.params.v[i].sub.value[1], ALEN(term->colors.table) - 1); } /* * RGB: 38:2::r:g:b[:ignored:tolerance:tolerance-color-space] * RGB: 38:2:r:g:b * * The second version is a "bastard" version - many * programs "forget" the color space ID * parameter... *sigh* */ else if (term->vt.params.v[i].sub.idx >= 4 && term->vt.params.v[i].sub.value[0] == 2) { const struct vt_param *param = &term->vt.params.v[i]; bool have_color_space_id = param->sub.idx >= 5; /* 0 - color space (ignored) */ int r_idx = 2 - !have_color_space_id; int g_idx = 3 - !have_color_space_id; int b_idx = 4 - !have_color_space_id; /* 5 - unused */ /* 6 - CS tolerance */ /* 7 - color space associated with tolerance */ uint8_t r = param->sub.value[r_idx]; uint8_t g = param->sub.value[g_idx]; uint8_t b = param->sub.value[b_idx]; src = COLOR_RGB; color = r << 16 | g << 8 | b; } /* Transparent: 38:1 */ /* CMY: 38:3::c:m:y[:tolerance:tolerance-color-space] */ /* CMYK: 38:4::c:m:y:k[:tolerance:tolerance-color-space] */ /* Unrecognized */ else { UNHANDLED_SGR(i); break; } if (unlikely(param == 58)) { term->vt.underline.color_src = src; term->vt.underline.color = color; term->bits_affecting_ascii_printer.underline_color = true; term_update_ascii_printer(term); } else if (param == 38) { term->vt.attrs.fg_src = src; term->vt.attrs.fg = color; } else { xassert(param == 48); term->vt.attrs.bg_src = src; term->vt.attrs.bg = color; } break; } case 39: term->vt.attrs.fg_src = COLOR_DEFAULT; break; /* Regular background colors */ case 40: case 41: case 42: case 43: case 44: case 45: case 46: case 47: term->vt.attrs.bg_src = COLOR_BASE16; term->vt.attrs.bg = param - 40; break; case 49: term->vt.attrs.bg_src = COLOR_DEFAULT; break; case 59: term->vt.underline.color_src = COLOR_DEFAULT; term->vt.underline.color = 0; term->bits_affecting_ascii_printer.underline_color = false; term_update_ascii_printer(term); break; /* Bright foreground colors */ case 90: case 91: case 92: case 93: case 94: case 95: case 96: case 97: term->vt.attrs.fg_src = COLOR_BASE16; term->vt.attrs.fg = param - 90 + 8; break; /* Bright background colors */ case 100: case 101: case 102: case 103: case 104: case 105: case 106: case 107: term->vt.attrs.bg_src = COLOR_BASE16; term->vt.attrs.bg = param - 100 + 8; break; default: UNHANDLED_SGR(i); break; } } } static void decset_decrst(struct terminal *term, unsigned param, bool enable) { #if defined(_DEBUG) /* For UNHANDLED() */ int UNUSED final = enable ? 'h' : 'l'; #endif /* Note: update XTSAVE/XTRESTORE if adding/removing things here */ switch (param) { case 1: /* DECCKM */ term->cursor_keys_mode = enable ? CURSOR_KEYS_APPLICATION : CURSOR_KEYS_NORMAL; break; case 5: /* DECSCNM */ term->reverse = enable; term_damage_all(term); term_damage_margins(term); break; case 6: { /* DECOM */ term->origin = enable ? ORIGIN_RELATIVE : ORIGIN_ABSOLUTE; term_cursor_home(term); break; } case 7: /* DECAWM */ term->auto_margin = enable; term->grid->cursor.lcf = false; break; case 9: if (enable) LOG_WARN("unimplemented: X10 mouse tracking mode"); #if 0 else if (term->mouse_tracking == MOUSE_X10) term->mouse_tracking = MOUSE_NONE; #endif break; case 12: term->cursor_blink.decset = enable; term_cursor_blink_update(term); break; case 25: /* DECTCEM */ term->hide_cursor = !enable; break; case 45: term->reverse_wrap = enable; break; case 66: /* DECNKM */ term->keypad_keys_mode = enable ? KEYPAD_APPLICATION : KEYPAD_NUMERICAL; break; case 67: if (enable) LOG_WARN("unimplemented: DECBKM"); break; case 80: term->sixel.scrolling = !enable; break; case 1000: if (enable) term->mouse_tracking = MOUSE_CLICK; else if (term->mouse_tracking == MOUSE_CLICK) term->mouse_tracking = MOUSE_NONE; term_xcursor_update(term); break; case 1001: if (enable) LOG_WARN("unimplemented: highlight mouse tracking"); break; case 1002: if (enable) term->mouse_tracking = MOUSE_DRAG; else if (term->mouse_tracking == MOUSE_DRAG) term->mouse_tracking = MOUSE_NONE; term_xcursor_update(term); break; case 1003: if (enable) term->mouse_tracking = MOUSE_MOTION; else if (term->mouse_tracking == MOUSE_MOTION) term->mouse_tracking = MOUSE_NONE; term_xcursor_update(term); break; case 1004: term->focus_events = enable; break; case 1005: if (enable) LOG_WARN("unimplemented: mouse reporting mode: UTF-8"); #if 0 else if (term->mouse_reporting == MOUSE_UTF8) term->mouse_reporting = MOUSE_NONE; #endif break; case 1006: if (enable) term->mouse_reporting = MOUSE_SGR; else if (term->mouse_reporting == MOUSE_SGR) term->mouse_reporting = MOUSE_NORMAL; break; case 1007: term->alt_scrolling = enable; break; case 1015: if (enable) term->mouse_reporting = MOUSE_URXVT; else if (term->mouse_reporting == MOUSE_URXVT) term->mouse_reporting = MOUSE_NORMAL; break; case 1016: if (enable) term->mouse_reporting = MOUSE_SGR_PIXELS; else if (term->mouse_reporting == MOUSE_SGR_PIXELS) term->mouse_reporting = MOUSE_NORMAL; break; case 1034: /* smm */ LOG_DBG("%s 8-bit meta mode", enable ? "enabling" : "disabling"); term->meta.eight_bit = enable; break; case 1035: /* numLock */ LOG_DBG("%s Num Lock modifier", enable ? "enabling" : "disabling"); term->num_lock_modifier = enable; break; case 1036: /* metaSendsEscape */ LOG_DBG("%s meta-sends-escape", enable ? "enabling" : "disabling"); term->meta.esc_prefix = enable; break; case 1042: term->bell_action_enabled = enable; break; #if 0 case 1043: LOG_WARN("unimplemented: raise window on ctrl-g"); break; #endif case 1048: if (enable) term_save_cursor(term); else term_restore_cursor(term, &term->grid->saved_cursor); break; case 47: case 1047: case 1049: if (enable && term->grid != &term->alt) { selection_cancel(term); if (param == 1049) term_save_cursor(term); term->grid = &term->alt; /* Cursor retains its position from the normal grid */ term_cursor_to( term, min(term->normal.cursor.point.row, term->rows - 1), min(term->normal.cursor.point.col, term->cols - 1)); tll_free(term->normal.scroll_damage); term_erase(term, 0, 0, term->rows - 1, term->cols - 1); } else if (!enable && term->grid == &term->alt) { selection_cancel(term); term->grid = &term->normal; /* Cursor retains its position from the alt grid */ term_cursor_to( term, min(term->alt.cursor.point.row, term->rows - 1), min(term->alt.cursor.point.col, term->cols - 1)); if (param == 1049) term_restore_cursor(term, &term->grid->saved_cursor); /* Delete all sixel images on the alt screen */ tll_foreach(term->alt.sixel_images, it) { sixel_destroy(&it->item); tll_remove(term->alt.sixel_images, it); } tll_free(term->alt.scroll_damage); term_damage_view(term); } term->bits_affecting_ascii_printer.sixels = tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); break; case 1070: term->sixel.use_private_palette = enable; break; case 2004: term->bracketed_paste = enable; break; case 2026: if (enable) term_enable_app_sync_updates(term); else term_disable_app_sync_updates(term); break; case 2027: term->grapheme_shaping = enable; break; case 2048: if (enable) term_enable_size_notifications(term); else term_disable_size_notifications(term); break; case 8452: term->sixel.cursor_right_of_graphics = enable; break; case 737769: if (enable) term_ime_enable(term); else { term_ime_disable(term); term->ime_reenable_after_url_mode = false; } break; default: UNHANDLED(); break; } } static void decset(struct terminal *term, unsigned param) { decset_decrst(term, param, true); } static void decrst(struct terminal *term, unsigned param) { decset_decrst(term, param, false); } /* * These values represent the current state of a DEC private mode, * as returned in the DECRPM reply to a DECRQM query. */ enum decrpm_status { DECRPM_NOT_RECOGNIZED = 0, DECRPM_SET = 1, DECRPM_RESET = 2, DECRPM_PERMANENTLY_SET = 3, DECRPM_PERMANENTLY_RESET = 4, }; static enum decrpm_status decrpm(bool enabled) { return enabled ? DECRPM_SET : DECRPM_RESET; } static enum decrpm_status decrqm(const struct terminal *term, unsigned param) { switch (param) { case 1: return decrpm(term->cursor_keys_mode == CURSOR_KEYS_APPLICATION); case 5: return decrpm(term->reverse); case 6: return decrpm(term->origin); case 7: return decrpm(term->auto_margin); case 9: return DECRPM_PERMANENTLY_RESET; /* term->mouse_tracking == MOUSE_X10; */ case 12: return decrpm(term->cursor_blink.decset); case 25: return decrpm(!term->hide_cursor); case 45: return decrpm(term->reverse_wrap); case 66: return decrpm(term->keypad_keys_mode == KEYPAD_APPLICATION); case 67: return DECRPM_PERMANENTLY_RESET; /* https://vt100.net/docs/vt510-rm/DECBKM */ case 80: return decrpm(!term->sixel.scrolling); case 1000: return decrpm(term->mouse_tracking == MOUSE_CLICK); case 1001: return DECRPM_PERMANENTLY_RESET; case 1002: return decrpm(term->mouse_tracking == MOUSE_DRAG); case 1003: return decrpm(term->mouse_tracking == MOUSE_MOTION); case 1004: return decrpm(term->focus_events); case 1005: return DECRPM_PERMANENTLY_RESET; /* term->mouse_reporting == MOUSE_UTF8; */ case 1006: return decrpm(term->mouse_reporting == MOUSE_SGR); case 1007: return decrpm(term->alt_scrolling); case 1015: return decrpm(term->mouse_reporting == MOUSE_URXVT); case 1016: return decrpm(term->mouse_reporting == MOUSE_SGR_PIXELS); case 1034: return decrpm(term->meta.eight_bit); case 1035: return decrpm(term->num_lock_modifier); case 1036: return decrpm(term->meta.esc_prefix); case 1042: return decrpm(term->bell_action_enabled); case 47: /* FALLTHROUGH */ case 1047: /* FALLTHROUGH */ case 1049: return decrpm(term->grid == &term->alt); case 1070: return decrpm(term->sixel.use_private_palette); case 2004: return decrpm(term->bracketed_paste); case 2026: return decrpm(term->render.app_sync_updates.enabled); case 2027: return term->conf->tweak.grapheme_width_method != GRAPHEME_WIDTH_DOUBLE ? DECRPM_PERMANENTLY_RESET : decrpm(term->grapheme_shaping); case 2048: return decrpm(term->size_notifications); case 8452: return decrpm(term->sixel.cursor_right_of_graphics); case 737769: return decrpm(term_ime_is_enabled(term)); } return DECRPM_NOT_RECOGNIZED; } static void xtsave(struct terminal *term, unsigned param) { switch (param) { case 1: term->xtsave.application_cursor_keys = term->cursor_keys_mode == CURSOR_KEYS_APPLICATION; break; case 5: term->xtsave.reverse = term->reverse; break; case 6: term->xtsave.origin = term->origin; break; case 7: term->xtsave.auto_margin = term->auto_margin; break; case 9: /* term->xtsave.mouse_x10 = term->mouse_tracking == MOUSE_X10; */ break; case 12: term->xtsave.cursor_blink = term->cursor_blink.decset; break; case 25: term->xtsave.show_cursor = !term->hide_cursor; break; case 45: term->xtsave.reverse_wrap = term->reverse_wrap; break; case 47: term->xtsave.alt_screen = term->grid == &term->alt; break; case 66: term->xtsave.application_keypad_keys = term->keypad_keys_mode == KEYPAD_APPLICATION; break; case 67: break; case 80: term->xtsave.sixel_display_mode = !term->sixel.scrolling; break; case 1000: term->xtsave.mouse_click = term->mouse_tracking == MOUSE_CLICK; break; case 1001: break; case 1002: term->xtsave.mouse_drag = term->mouse_tracking == MOUSE_DRAG; break; case 1003: term->xtsave.mouse_motion = term->mouse_tracking == MOUSE_MOTION; break; case 1004: term->xtsave.focus_events = term->focus_events; break; case 1005: /* term->xtsave.mouse_utf8 = term->mouse_reporting == MOUSE_UTF8; */ break; case 1006: term->xtsave.mouse_sgr = term->mouse_reporting == MOUSE_SGR; break; case 1007: term->xtsave.alt_scrolling = term->alt_scrolling; break; case 1015: term->xtsave.mouse_urxvt = term->mouse_reporting == MOUSE_URXVT; break; case 1016: term->xtsave.mouse_sgr_pixels = term->mouse_reporting == MOUSE_SGR_PIXELS; break; case 1034: term->xtsave.meta_eight_bit = term->meta.eight_bit; break; case 1035: term->xtsave.num_lock_modifier = term->num_lock_modifier; break; case 1036: term->xtsave.meta_esc_prefix = term->meta.esc_prefix; break; case 1042: term->xtsave.bell_action_enabled = term->bell_action_enabled; break; case 1047: term->xtsave.alt_screen = term->grid == &term->alt; break; case 1048: term_save_cursor(term); break; case 1049: term->xtsave.alt_screen = term->grid == &term->alt; break; case 1070: term->xtsave.sixel_private_palette = term->sixel.use_private_palette; break; case 2004: term->xtsave.bracketed_paste = term->bracketed_paste; break; case 2026: term->xtsave.app_sync_updates = term->render.app_sync_updates.enabled; break; case 2027: term->xtsave.grapheme_shaping = term->grapheme_shaping; break; case 2048: term->xtsave.size_notifications = term->size_notifications; break; case 8452: term->xtsave.sixel_cursor_right_of_graphics = term->sixel.cursor_right_of_graphics; break; case 737769: term->xtsave.ime = term_ime_is_enabled(term); break; } } static void xtrestore(struct terminal *term, unsigned param) { bool enable; switch (param) { case 1: enable = term->xtsave.application_cursor_keys; break; case 5: enable = term->xtsave.reverse; break; case 6: enable = term->xtsave.origin; break; case 7: enable = term->xtsave.auto_margin; break; case 9: /* enable = term->xtsave.mouse_x10; break; */ return; case 12: enable = term->xtsave.cursor_blink; break; case 25: enable = term->xtsave.show_cursor; break; case 45: enable = term->xtsave.reverse_wrap; break; case 47: enable = term->xtsave.alt_screen; break; case 66: enable = term->xtsave.application_keypad_keys; break; case 67: return; case 80: enable = term->xtsave.sixel_display_mode; break; case 1000: enable = term->xtsave.mouse_click; break; case 1001: return; case 1002: enable = term->xtsave.mouse_drag; break; case 1003: enable = term->xtsave.mouse_motion; break; case 1004: enable = term->xtsave.focus_events; break; case 1005: /* enable = term->xtsave.mouse_utf8; break; */ return; case 1006: enable = term->xtsave.mouse_sgr; break; case 1007: enable = term->xtsave.alt_scrolling; break; case 1015: enable = term->xtsave.mouse_urxvt; break; case 1016: enable = term->xtsave.mouse_sgr_pixels; break; case 1034: enable = term->xtsave.meta_eight_bit; break; case 1035: enable = term->xtsave.num_lock_modifier; break; case 1036: enable = term->xtsave.meta_esc_prefix; break; case 1042: enable = term->xtsave.bell_action_enabled; break; case 1047: enable = term->xtsave.alt_screen; break; case 1048: enable = true; break; case 1049: enable = term->xtsave.alt_screen; break; case 1070: enable = term->xtsave.sixel_private_palette; break; case 2004: enable = term->xtsave.bracketed_paste; break; case 2026: enable = term->xtsave.app_sync_updates; break; case 2027: enable = term->xtsave.grapheme_shaping; break; case 2048: enable = term->xtsave.size_notifications; break; case 8452: enable = term->xtsave.sixel_cursor_right_of_graphics; break; case 737769: enable = term->xtsave.ime; break; default: return; } decset_decrst(term, param, enable); } static bool params_to_rectangular_area(const struct terminal *term, int first_idx, int *top, int *left, int *bottom, int *right) { int rel_top = vt_param_get(term, first_idx + 0, 1) - 1; *left = min(vt_param_get(term, first_idx + 1, 1) - 1, term->cols - 1); int rel_bottom = vt_param_get(term, first_idx + 2, term->rows) - 1; *right = min(vt_param_get(term, first_idx + 3, term->cols) - 1, term->cols - 1); if (rel_top > rel_bottom || *left > *right) return false; *top = term_row_rel_to_abs(term, rel_top); *bottom = term_row_rel_to_abs(term, rel_bottom); return true; } void csi_dispatch(struct terminal *term, uint8_t final) { LOG_DBG("%s (%08x)", csi_as_string(term, final, -1), term->vt.private); switch (term->vt.private) { case 0: { switch (final) { case 'b': if (term->vt.last_printed != 0) { /* * Note: we never reset 'last-printed'. According to * ECMA-48, the behaviour is undefined if REP was * _not_ preceded by a graphical character. */ int count = vt_param_get(term, 0, 1); LOG_DBG("REP: '%lc' %d times", (wint_t)term->vt.last_printed, count); const int width = c32width(term->vt.last_printed); if (width > 0) { for (int i = 0; i < count; i++) term_print(term, term->vt.last_printed, width, false); } } break; case 'c': { if (vt_param_get(term, 0, 0) != 0) { UNHANDLED(); break; } /* Send Device Attributes (Primary DA) */ /* * Responses: * - CSI?1;2c vt100 with advanced video option * - CSI?1;0c vt101 with no options * - CSI?6c vt102 * - CSI?62;c vt220 * - CSI?63;c vt320 * - CSI?64;c vt420 * * Ps (response may contain multiple): * - 1 132 columns * - 2 Printer. * - 3 ReGIS graphics. * - 4 Sixel graphics. * - 6 Selective erase. * - 8 User-defined keys. * - 9 National Replacement Character sets. * - 15 Technical characters. * - 16 Locator port. * - 17 Terminal state interrogation. * - 18 User windows. * - 21 Horizontal scrolling. * - 22 ANSI color, e.g., VT525. * - 28 Rectangular editing. * - 29 ANSI text locator (i.e., DEC Locator mode). * * Note: we report ourselves as a VT220, mainly to be able * to pass parameters, to indicate we support sixel, and * ANSI colors. * * The VT level must be synchronized with the secondary DA * response. * * Note: tertiary DA responds with "FOOT". */ if (term->conf->tweak.sixel) { static const char reply[] = "\033[?62;4;22;28c"; term_to_slave(term, reply, sizeof(reply) - 1); } else { static const char reply[] = "\033[?62;22;28c"; term_to_slave(term, reply, sizeof(reply) - 1); } break; } case 'd': { /* VPA - vertical line position absolute */ int rel_row = vt_param_get(term, 0, 1) - 1; int row = term_row_rel_to_abs(term, rel_row); term_cursor_to(term, row, term->grid->cursor.point.col); break; } case 'm': csi_sgr(term); break; case 'A': term_cursor_up(term, vt_param_get(term, 0, 1)); break; case 'e': case 'B': term_cursor_down(term, vt_param_get(term, 0, 1)); break; case 'a': case 'C': term_cursor_right(term, vt_param_get(term, 0, 1)); break; case 'D': term_cursor_left(term, vt_param_get(term, 0, 1)); break; case 'E': /* CNL - Cursor Next Line */ term_cursor_down(term, vt_param_get(term, 0, 1)); term_cursor_left(term, term->grid->cursor.point.col); break; case 'F': /* CPL - Cursor Previous Line */ term_cursor_up(term, vt_param_get(term, 0, 1)); term_cursor_left(term, term->grid->cursor.point.col); break; case 'g': { int param = vt_param_get(term, 0, 0); switch (param) { case 0: /* Clear tab stop at *current* column */ tll_foreach(term->tab_stops, it) { if (it->item == term->grid->cursor.point.col) tll_remove(term->tab_stops, it); else if (it->item > term->grid->cursor.point.col) break; } break; case 3: /* Clear *all* tabs */ tll_free(term->tab_stops); break; default: UNHANDLED(); break; } break; } case '`': case 'G': { /* Cursor horizontal absolute */ int col = min(vt_param_get(term, 0, 1), term->cols) - 1; term_cursor_col(term, col); break; } case 'f': case 'H': { /* Move cursor */ int rel_row = vt_param_get(term, 0, 1) - 1; int row = term_row_rel_to_abs(term, rel_row); int col = min(vt_param_get(term, 1, 1), term->cols) - 1; term_cursor_to(term, row, col); break; } case 'J': { /* Erase screen */ int param = vt_param_get(term, 0, 0); switch (param) { case 0: { /* From cursor to end of screen */ const struct coord *cursor = &term->grid->cursor.point; term_erase( term, cursor->row, cursor->col, term->rows - 1, term->cols - 1); term->grid->cursor.lcf = false; break; } case 1: { /* From start of screen to cursor */ const struct coord *cursor = &term->grid->cursor.point; term_erase(term, 0, 0, cursor->row, cursor->col); term->grid->cursor.lcf = false; break; } case 2: /* Erase entire screen */ term_erase(term, 0, 0, term->rows - 1, term->cols - 1); term->grid->cursor.lcf = false; break; case 3: { /* Erase scrollback */ term_erase_scrollback(term); break; } default: UNHANDLED(); break; } break; } case 'K': { /* Erase line */ int param = vt_param_get(term, 0, 0); switch (param) { case 0: { /* From cursor to end of line */ const struct coord *cursor = &term->grid->cursor.point; term_erase( term, cursor->row, cursor->col, cursor->row, term->cols - 1); term->grid->cursor.lcf = false; break; } case 1: { /* From start of line to cursor */ const struct coord *cursor = &term->grid->cursor.point; term_erase(term, cursor->row, 0, cursor->row, cursor->col); term->grid->cursor.lcf = false; break; } case 2: { /* Entire line */ const struct coord *cursor = &term->grid->cursor.point; term_erase(term, cursor->row, 0, cursor->row, term->cols - 1); term->grid->cursor.lcf = false; break; } default: UNHANDLED(); break; } break; } case 'L': { /* IL */ if (term->grid->cursor.point.row < term->scroll_region.start || term->grid->cursor.point.row >= term->scroll_region.end) break; int count = min( vt_param_get(term, 0, 1), term->scroll_region.end - term->grid->cursor.point.row); term_scroll_reverse_partial( term, (struct scroll_region){ .start = term->grid->cursor.point.row, .end = term->scroll_region.end}, count); term->grid->cursor.lcf = false; term->grid->cursor.point.col = 0; break; } case 'M': { /* DL */ if (term->grid->cursor.point.row < term->scroll_region.start || term->grid->cursor.point.row >= term->scroll_region.end) break; int count = min( vt_param_get(term, 0, 1), term->scroll_region.end - term->grid->cursor.point.row); term_scroll_partial( term, (struct scroll_region){ .start = term->grid->cursor.point.row, .end = term->scroll_region.end}, count); term->grid->cursor.lcf = false; term->grid->cursor.point.col = 0; break; } case 'P': { /* DCH: Delete character(s) */ /* Number of characters to delete */ int count = min( vt_param_get(term, 0, 1), term->cols - term->grid->cursor.point.col); /* Number of characters left after deletion (on current line) */ int remaining = term->cols - (term->grid->cursor.point.col + count); /* 'Delete' characters by moving the remaining ones */ memmove(&term->grid->cur_row->cells[term->grid->cursor.point.col], &term->grid->cur_row->cells[term->grid->cursor.point.col + count], remaining * sizeof(term->grid->cur_row->cells[0])); for (size_t c = 0; c < remaining; c++) term->grid->cur_row->cells[term->grid->cursor.point.col + c].attrs.clean = 0; term->grid->cur_row->dirty = true; /* Erase the remainder of the line */ const struct coord *cursor = &term->grid->cursor.point; term_erase( term, cursor->row, cursor->col + remaining, cursor->row, term->cols - 1); term->grid->cursor.lcf = false; break; } case '@': { /* ICH: insert character(s) */ /* Number of characters to insert */ int count = min( vt_param_get(term, 0, 1), term->cols - term->grid->cursor.point.col); /* Characters to move */ int remaining = term->cols - (term->grid->cursor.point.col + count); /* Push existing characters */ memmove(&term->grid->cur_row->cells[term->grid->cursor.point.col + count], &term->grid->cur_row->cells[term->grid->cursor.point.col], remaining * sizeof(term->grid->cur_row->cells[0])); for (size_t c = 0; c < remaining; c++) term->grid->cur_row->cells[term->grid->cursor.point.col + count + c].attrs.clean = 0; term->grid->cur_row->dirty = true; /* Erase (insert space characters) */ const struct coord *cursor = &term->grid->cursor.point; term_erase( term, cursor->row, cursor->col, cursor->row, cursor->col + count - 1); term->grid->cursor.lcf = false; break; } case 'S': { const struct scroll_region *r = &term->scroll_region; int amount = min(vt_param_get(term, 0, 1), r->end - r->start); term_scroll(term, amount); break; } case 'T': { const struct scroll_region *r = &term->scroll_region; int amount = min(vt_param_get(term, 0, 1), r->end - r->start); term_scroll_reverse(term, amount); break; } case 'X': { /* Erase chars */ int count = min( vt_param_get(term, 0, 1), term->cols - term->grid->cursor.point.col); const struct coord *cursor = &term->grid->cursor.point; term_erase( term, cursor->row, cursor->col, cursor->row, cursor->col + count - 1); term->grid->cursor.lcf = false; break; } case 'I': { /* CHT - Tab Forward (param is number of tab stops to move through) */ for (int i = 0; i < vt_param_get(term, 0, 1); i++) { int new_col = term->cols - 1; tll_foreach(term->tab_stops, it) { if (it->item > term->grid->cursor.point.col) { new_col = it->item; break; } } xassert(new_col >= term->grid->cursor.point.col); bool lcf = term->grid->cursor.lcf; term_cursor_right(term, new_col - term->grid->cursor.point.col); term->grid->cursor.lcf = lcf; } break; } case 'Z': /* CBT - Back tab (param is number of tab stops to move back through) */ for (int i = 0; i < vt_param_get(term, 0, 1); i++) { int new_col = 0; tll_rforeach(term->tab_stops, it) { if (it->item < term->grid->cursor.point.col) { new_col = it->item; break; } } xassert(term->grid->cursor.point.col >= new_col); term_cursor_left(term, term->grid->cursor.point.col - new_col); } break; case 'h': case 'l': { /* Set/Reset Mode (SM/RM) */ int param = vt_param_get(term, 0, 0); bool sm = final == 'h'; if (param == 4) { /* Insertion Replacement Mode (IRM) */ term->insert_mode = sm; term->bits_affecting_ascii_printer.insert_mode = sm; term_update_ascii_printer(term); break; } /* * ECMA-48 defines modes 1-22, all of which were optional * (§7.1; "may have one state only") and are considered * deprecated (§7.1) in the latest (5th) edition. xterm only * documents modes 2, 4, 12 and 20, the last of which was * outright removed (§8.3.106) in 5th edition ECMA-48. */ if (sm) { LOG_WARN("SM with unimplemented mode: %d", param); } break; } case 'r': { int start = vt_param_get(term, 0, 1); int end = min(vt_param_get(term, 1, term->rows), term->rows); if (end > start) { /* 1-based */ term->scroll_region.start = start - 1; term->scroll_region.end = end; term_cursor_home(term); LOG_DBG("scroll region: %d-%d", term->scroll_region.start, term->scroll_region.end); } break; } case 's': term_save_cursor(term); break; case 'u': term_restore_cursor(term, &term->grid->saved_cursor); break; case 't': { /* * Window operations */ const unsigned param = vt_param_get(term, 0, 0); switch (param) { case 1: LOG_WARN("unimplemented: de-iconify"); break; case 2: LOG_WARN("unimplemented: iconify"); break; case 3: LOG_WARN("unimplemented: move window to pixel position"); break; case 4: LOG_WARN("unimplemented: resize window in pixels"); break; case 5: LOG_WARN("unimplemented: raise window to front of stack"); break; case 6: LOG_WARN("unimplemented: raise window to back of stack"); break; case 7: LOG_WARN("unimplemented: refresh window"); break; case 8: LOG_WARN("unimplemented: resize window in chars"); break; case 9: LOG_WARN("unimplemented: maximize/unmaximize window"); break; case 10: LOG_WARN("unimplemented: to/from full screen"); break; case 20: LOG_WARN("unimplemented: report icon label"); break; case 24: LOG_WARN("unimplemented: resize window (DECSLPP)"); break; case 11: /* report if window is iconified */ /* We don't know - always report *not* iconified */ /* 1=not iconified, 2=iconified */ term_to_slave(term, "\033[1t", 4); break; case 13: { /* report window position */ /* We don't know our position - always report (0,0) */ static const char reply[] = "\033[3;0;0t"; switch (vt_param_get(term, 1, 0)) { case 0: /* window position */ case 2: /* text area position */ term_to_slave(term, reply, sizeof(reply) - 1); break; default: UNHANDLED(); break; } break; } case 14: { /* report window size in pixels */ int width = -1; int height = -1; switch (vt_param_get(term, 1, 0)) { case 0: /* text area size */ width = term->width - term->margins.left - term->margins.right; height = term->height - term->margins.top - term->margins.bottom; break; case 2: /* window size */ width = term->width; height = term->height; break; default: UNHANDLED(); break; } if (width >= 0 && height >= 0) { char reply[64]; size_t n = xsnprintf( reply, sizeof(reply), "\033[4;%d;%dt", height, width); term_to_slave(term, reply, n); } break; } case 15: /* report screen size in pixels */ tll_foreach(term->window->on_outputs, it) { char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\033[5;%d;%dt", it->item->dim.px_real.height, it->item->dim.px_real.width); term_to_slave(term, reply, n); break; } if (tll_length(term->window->on_outputs) == 0) term_to_slave(term, "\033[5;0;0t", 8); break; case 16: { /* report cell size in pixels */ char reply[64]; size_t n = xsnprintf( reply, sizeof(reply), "\033[6;%d;%dt", term->cell_height, term->cell_width); term_to_slave(term, reply, n); break; } case 18: { /* text area size in chars */ char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\033[8;%d;%dt", term->rows, term->cols); term_to_slave(term, reply, n); break; } case 19: { /* report screen size in chars */ tll_foreach(term->window->on_outputs, it) { char reply[64]; size_t n = xsnprintf( reply, sizeof(reply), "\033[9;%d;%dt", it->item->dim.px_real.height / term->cell_height, it->item->dim.px_real.width / term->cell_width); term_to_slave(term, reply, n); break; } if (tll_length(term->window->on_outputs) == 0) term_to_slave(term, "\033[9;0;0t", 8); break; } case 21: { #if 0 /* Disabled for now, see #1894 */ char reply[3 + strlen(term->window_title) + 2 + 1]; int chars = xsnprintf( reply, sizeof(reply), "\033]l%s\033\\", term->window_title); term_to_slave(term, reply, chars); #else LOG_WARN("CSI 21 t (report window title) ignored"); #endif break; } case 22: { /* push window title */ /* 0 - icon + title, 1 - icon, 2 - title */ unsigned what = vt_param_get(term, 1, 0); if (what == 0 || what == 2) { tll_push_back( term->window_title_stack, xstrdup(term->window_title)); } break; } case 23: { /* pop window title */ /* 0 - icon + title, 1 - icon, 2 - title */ unsigned what = vt_param_get(term, 1, 0); if (what == 0 || what == 2) { if (tll_length(term->window_title_stack) > 0) { char *title = tll_pop_back(term->window_title_stack); term_set_window_title(term, title); free(title); } } break; } case 1001: { } default: LOG_DBG("ignoring %s", csi_as_string(term, final, -1)); break; } break; } case 'n': { if (term->vt.params.idx > 0) { int param = vt_param_get(term, 0, 0); switch (param) { case 5: /* Query device status */ term_to_slave(term, "\x1b[0n", 4); /* "Device OK" */ break; case 6: { /* u7 - cursor position query */ int row = term->origin == ORIGIN_ABSOLUTE ? term->grid->cursor.point.row : term->grid->cursor.point.row - term->scroll_region.start; /* TODO: we use 0-based position, while the xterm * terminfo says the receiver of the reply should * decrement, hence we must add 1 */ char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\x1b[%d;%dR", row + 1, term->grid->cursor.point.col + 1); term_to_slave(term, reply, n); break; } default: UNHANDLED(); break; } } else UNHANDLED(); break; } default: UNHANDLED(); break; } break; /* private[0] == 0 */ } case '?': { switch (final) { case 'h': /* DECSET - DEC private mode set */ for (size_t i = 0; i < term->vt.params.idx; i++) decset(term, term->vt.params.v[i].value); break; case 'l': /* DECRST - DEC private mode reset */ for (size_t i = 0; i < term->vt.params.idx; i++) decrst(term, term->vt.params.v[i].value); break; case 's': for (size_t i = 0; i < term->vt.params.idx; i++) xtsave(term, term->vt.params.v[i].value); break; case 'r': for (size_t i = 0; i < term->vt.params.idx; i++) xtrestore(term, term->vt.params.v[i].value); break; case 'S': { if (!term->conf->tweak.sixel) { UNHANDLED(); break; } unsigned target = vt_param_get(term, 0, 0); unsigned operation = vt_param_get(term, 1, 0); switch (target) { case 1: switch (operation) { case 1: sixel_colors_report_current(term); break; case 2: sixel_colors_reset(term); break; case 3: sixel_colors_set(term, vt_param_get(term, 2, 0)); break; case 4: sixel_colors_report_max(term); break; default: UNHANDLED(); break; } break; case 2: switch (operation) { case 1: sixel_geometry_report_current(term); break; case 2: sixel_geometry_reset(term); break; case 3: sixel_geometry_set(term, vt_param_get(term, 2, 0), vt_param_get(term, 3, 0)); break; case 4: sixel_geometry_report_max(term); break; default: UNHANDLED(); break; } break; default: UNHANDLED(); break; } break; } case 'm': { int resource = vt_param_get(term, 0, 0); int value = -1; switch (resource) { case 0: /* modifyKeyboard */ value = 0; break; case 1: /* modifyCursorKeys */ case 2: /* modifyFunctionKeys */ value = 1; break; case 4: /* modifyOtherKeys */ value = term->modify_other_keys_2 ? 2 : 1; break; default: LOG_WARN("XTQMODKEYS: invalid resource '%d' in '%s'", resource, csi_as_string(term, final, -1)); break; } if (value >= 0) { char reply[16] = {0}; int chars = snprintf(reply, sizeof(reply), "\033[>%d;%dm", resource, value); term_to_slave(term, reply, chars); } break; } case 'p': { /* * Request status of ECMA-48/"ANSI" private mode (DECRQM * for SM/RM modes; see private="?$" case further below for * DECSET/DECRST modes) */ unsigned param = vt_param_get(term, 0, 0); unsigned status = DECRPM_NOT_RECOGNIZED; if (param == 4) { status = decrpm(term->insert_mode); } char reply[32]; size_t n = xsnprintf(reply, sizeof(reply), "\033[%u;%u$y", param, status); term_to_slave(term, reply, n); break; } case 'u': { enum kitty_kbd_flags flags = term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx]; char reply[8]; int chars = snprintf(reply, sizeof(reply), "\033[?%uu", flags); term_to_slave(term, reply, chars); break; } default: UNHANDLED(); break; } break; /* private[0] == '?' */ } case '>': { switch (final) { case 'c': /* Send Device Attributes (Secondary DA) */ if (vt_param_get(term, 0, 0) != 0) { UNHANDLED(); break; } /* * Param 1 - terminal type: * 0 - vt100 * 1 - vt220 * 2 - vt240 * 18 - vt330 * 19 - vt340 * 24 - vt320 * 41 - vt420 * 61 - vt510 * 64 - vt520 * 65 - vt525 * * Param 2 - firmware version * xterm uses its version number. We use an xterm * version number too, since e.g. Emacs uses this to * determine level of support. * * We report ourselves as a VT220. This must be * synchronized with the primary DA response. * * Note: tertiary DA replies with "FOOT". */ static_assert(FOOT_MAJOR < 100, "Major version must not exceed 99"); static_assert(FOOT_MINOR < 100, "Minor version must not exceed 99"); static_assert(FOOT_PATCH < 100, "Patch version must not exceed 99"); char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\033[>1;%02u%02u%02u;0c", FOOT_MAJOR, FOOT_MINOR, FOOT_PATCH); term_to_slave(term, reply, n); break; case 'm': if (term->vt.params.idx == 0) { /* Reset all */ } else { int resource = vt_param_get(term, 0, 0); int value = vt_param_get(term, 1, -1); switch (resource) { case 0: /* modifyKeyboard */ break; case 1: /* modifyCursorKeys */ case 2: /* modifyFunctionKeys */ /* Ignored, we always report modifiers */ if (value != 2 && value != -1) { LOG_WARN( "unimplemented: %s = %d", resource == 1 ? "modifyCursorKeys" : resource == 2 ? "modifyFunctionKeys" : "", value); } break; case 4: /* modifyOtherKeys */ term->modify_other_keys_2 = value == 2; LOG_DBG("modifyOtherKeys=%d", value); break; default: LOG_WARN("XTMODKEYS: invalid resource '%d' in '%s'", resource, csi_as_string(term, final, -1)); break; } } break; /* final == 'm' */ case 'n': { int resource = vt_param_get(term, 0, 2); /* Default is modifyFunctionKeys */ switch (resource) { case 0: /* modifyKeyboard */ case 1: /* modifyCursorKeys */ case 2: /* modifyFunctionKeys */ break; case 4: /* modifyOtherKeys */ /* We don't support fully disabling modifyOtherKeys, * but simply revert back to mode '1' */ term->modify_other_keys_2 = false; LOG_DBG("modifyOtherKeys=1"); break; } break; } case 'u': { int flags = vt_param_get(term, 0, 0) & KITTY_KBD_SUPPORTED; struct grid *grid = term->grid; uint8_t idx = grid->kitty_kbd.idx; if (idx + 1 >= ALEN(grid->kitty_kbd.flags)) { /* Stack full, evict oldest by wrapping around */ idx = 0; } else idx++; grid->kitty_kbd.flags[idx] = flags; grid->kitty_kbd.idx = idx; LOG_DBG("kitty kbd: pushed new flags: 0x%03x", flags); break; } case 'q': { /* XTVERSION */ if (vt_param_get(term, 0, 0) != 0) { UNHANDLED(); break; } char reply[64]; size_t n = xsnprintf( reply, sizeof(reply), "\033P>|foot(%u.%u.%u%s%s)\033\\", FOOT_MAJOR, FOOT_MINOR, FOOT_PATCH, FOOT_EXTRA[0] != '\0' ? "-" : "", FOOT_EXTRA); term_to_slave(term, reply, n); break; } default: UNHANDLED(); break; } break; /* private[0] == '>' */ } case '<': { switch (final) { case 'u': { int count = vt_param_get(term, 0, 1); LOG_DBG("kitty kbd: popping %d levels of flags", count); struct grid *grid = term->grid; uint8_t idx = grid->kitty_kbd.idx; for (int i = 0; i < count; i++) { /* Reset flags. This ensures we get flags=0 when * over-popping */ grid->kitty_kbd.flags[idx] = 0; if (idx == 0) idx = ALEN(grid->kitty_kbd.flags) - 1; else idx--; } grid->kitty_kbd.idx = idx; LOG_DBG("kitty kbd: flags after pop: 0x%03x", term->grid->kitty_kbd.flags[idx]); break; } } break; /* private[0] == '<' */ } case ' ': { switch (final) { case 'q': { int param = vt_param_get(term, 0, 0); switch (param) { case 0: /* blinking block, but we use it to reset to configured default */ term->cursor_style = term->conf->cursor.style; term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; term_cursor_blink_update(term); break; case 1: /* blinking block */ case 2: /* steady block */ term->cursor_style = term->conf->cursor.style == CURSOR_HOLLOW ? CURSOR_HOLLOW : CURSOR_BLOCK; break; case 3: /* blinking underline */ case 4: /* steady underline */ term->cursor_style = CURSOR_UNDERLINE; break; case 5: /* blinking bar */ case 6: /* steady bar */ term->cursor_style = CURSOR_BEAM; break; default: UNHANDLED(); break; } if (param > 0 && param <= 6) { term->cursor_blink.deccsusr = param & 1; term_cursor_blink_update(term); } break; } default: UNHANDLED(); break; } break; /* private[0] == ' ' */ } case '!': { if (final == 'p') { term_reset(term, false); break; } UNHANDLED(); break; /* private[0] == '!' */ } case '=': { switch (final) { case 'c': if (vt_param_get(term, 0, 0) != 0) { UNHANDLED(); break; } /* * Send Device Attributes (Tertiary DA) * * Reply format is "DCS ! | DDDDDDDD ST" * * D..D is the unit ID of the terminal, consisting of four * hexadecimal pairs. The first pair represents the * manufacturing site code. This code can be any * hexadecimal value from 00 through FF. */ term_to_slave(term, "\033P!|464f4f54\033\\", 14); /* FOOT */ break; case 'u': { int flag_set = vt_param_get(term, 0, 0) & KITTY_KBD_SUPPORTED; int mode = vt_param_get(term, 1, 1); struct grid *grid = term->grid; uint8_t idx = grid->kitty_kbd.idx; switch (mode) { case 1: /* set bits are set, unset bits are reset */ grid->kitty_kbd.flags[idx] = flag_set; break; case 2: /* set bits are set, unset bits are left unchanged */ grid->kitty_kbd.flags[idx] |= flag_set; break; case 3: /* set bits are reset, unset bits are left unchanged */ grid->kitty_kbd.flags[idx] &= ~flag_set; break; default: UNHANDLED(); break; } LOG_DBG("kitty kbd: flags after update: 0x%03x", grid->kitty_kbd.flags[idx]); break; } default: UNHANDLED(); break; } break; /* private[0] == '=' */ } case '$': { switch (final) { case 'r': { /* DECCARA */ int top, left, bottom, right; if (!params_to_rectangular_area( term, 0, &top, &left, &bottom, &right)) { break; } for (int r = top; r <= bottom; r++) { struct row *row = grid_row(term->grid, r); row->dirty = true; for (int c = left; c <= right; c++) { struct attributes *a = &row->cells[c].attrs; a->clean = 0; for (size_t i = 4; i < term->vt.params.idx; i++) { const int param = term->vt.params.v[i].value; /* DECCARA only supports a sub-set of SGR parameters */ switch (param) { case 0: a->bold = false; a->underline = false; a->blink = false; a->reverse = false; break; case 1: a->bold = true; break; case 4: a->underline = true; break; case 5: a->blink = true; break; case 7: a->reverse = true; break; case 22: a->bold = false; break; case 24: a->underline = false; break; case 25: a->blink = false; break; case 27: a->reverse = false; break; } } } } break; } case 't': { /* DECRARA */ int top, left, bottom, right; if (!params_to_rectangular_area( term, 0, &top, &left, &bottom, &right)) { break; } for (int r = top; r <= bottom; r++) { struct row *row = grid_row(term->grid, r); row->dirty = true; for (int c = left; c <= right; c++) { struct attributes *a = &row->cells[c].attrs; a->clean = 0; for (size_t i = 4; i < term->vt.params.idx; i++) { const int param = term->vt.params.v[i].value; /* DECRARA only supports a sub-set of SGR parameters */ switch (param) { case 0: a->bold = !a->bold; a->underline = !a->underline; a->blink = !a->blink; a->reverse = !a->reverse; break; case 1: a->bold = !a->bold; break; case 4: a->underline = !a->underline; break; case 5: a->blink = !a->blink; break; case 7: a->reverse = !a->reverse; break; } } } } break; } case 'v': { /* DECCRA */ int src_top, src_left, src_bottom, src_right; if (!params_to_rectangular_area( term, 0, &src_top, &src_left, &src_bottom, &src_right)) { break; } int src_page = vt_param_get(term, 4, 1); int dst_rel_top = vt_param_get(term, 5, 1) - 1; int dst_left = vt_param_get(term, 6, 1) - 1; int dst_page = vt_param_get(term, 7, 1); if (unlikely(src_page != 1 || dst_page != 1)) { /* We don’t support “pages” */ break; } int dst_rel_bottom = dst_rel_top + (src_bottom - src_top); int dst_right = min(dst_left + (src_right - src_left), term->cols - 1); int dst_top = term_row_rel_to_abs(term, dst_rel_top); int dst_bottom = term_row_rel_to_abs(term, dst_rel_bottom); /* Target area outside the screen is clipped */ const size_t row_count = min(src_bottom - src_top, dst_bottom - dst_top) + 1; const size_t cell_count = min(src_right - src_left, dst_right - dst_left) + 1; sixel_overwrite_by_rectangle( term, dst_top, dst_left, row_count, cell_count); /* * Copy source area * * Note: since source and destination may overlap, we need * to copy out the entire source region first, and _then_ * write the destination. I.e. this is similar to how * memmove() behaves, but adapted to our row/cell * structure. */ struct cell **copy = xmalloc(row_count * sizeof(copy[0])); for (int r = 0; r < row_count; r++) { copy[r] = xmalloc(cell_count * sizeof(copy[r][0])); const struct row *row = grid_row(term->grid, src_top + r); const struct cell *cell = &row->cells[src_left]; memcpy(copy[r], cell, cell_count * sizeof(copy[r][0])); } /* Paste into destination area */ for (int r = 0; r < row_count; r++) { struct row *row = grid_row(term->grid, dst_top + r); row->dirty = true; struct cell *cell = &row->cells[dst_left]; memcpy(cell, copy[r], cell_count * sizeof(copy[r][0])); free(copy[r]); for (;cell < &row->cells[dst_left + cell_count]; cell++) cell->attrs.clean = 0; if (unlikely(row->extra != NULL)) { /* TODO: technically, we should copy the source URIs... */ grid_row_uri_range_erase(row, dst_left, dst_right); } } free(copy); break; } case 'x': { /* DECFRA */ const uint8_t c = vt_param_get(term, 0, 0); if (unlikely(!((c >= 32 && c < 126) || c >= 160))) break; int top, left, bottom, right; if (!params_to_rectangular_area( term, 1, &top, &left, &bottom, &right)) { break; } /* Erase the entire region at once (MUCH cheaper than * doing it row by row, or even character by * character). */ sixel_overwrite_by_rectangle( term, top, left, bottom - top + 1, right - left + 1); for (int r = top; r <= bottom; r++) term_fill(term, r, left, c, right - left + 1, true); break; } case 'z': { /* DECERA */ int top, left, bottom, right; if (!params_to_rectangular_area( term, 0, &top, &left, &bottom, &right)) { break; } /* * Note: term_erase() _also_ erases sixels, but since * we’re forced to erase one row at a time, erasing the * entire sixel here is more efficient. */ sixel_overwrite_by_rectangle( term, top, left, bottom - top + 1, right - left + 1); for (int r = top; r <= bottom; r++) term_erase(term, r, left, r, right); break; } } break; /* private[0] == ‘$’ */ } case '#': { switch (final) { case 'P': { /* XTPUSHCOLORS */ int slot = vt_param_get(term, 0, 0); /* Pm == 0, "push" (what xterm does is take take the *current* slot + 1, even if that's in the middle of the stack, and overwrites whatever is already in that slot) */ if (slot == 0) slot = term->color_stack.idx + 1; if (term->color_stack.size < slot) { const size_t new_size = slot; term->color_stack.stack = xrealloc( term->color_stack.stack, new_size * sizeof(term->color_stack.stack[0])); /* Initialize new slots (except the selected slot, which is done below) */ xassert(new_size > 0); for (size_t i = term->color_stack.size; i < new_size - 1; i++) { memcpy(&term->color_stack.stack[i], &term->colors, sizeof(term->colors)); } term->color_stack.size = new_size; } xassert(slot > 0); xassert(slot <= term->color_stack.size); term->color_stack.idx = slot; memcpy(&term->color_stack.stack[slot - 1], &term->colors, sizeof(term->colors)); break; } case 'Q': { /* XTPOPCOLORS */ int slot = vt_param_get(term, 0, 0); /* Pm == 0, "pop" (what xterm does is copy colors from the *current* slot, *and* decrease the current slot index, even if that's in the middle of the stack) */ if (slot == 0) slot = term->color_stack.idx; if (slot > 0 && slot <= term->color_stack.size) { memcpy(&term->colors, &term->color_stack.stack[slot - 1], sizeof(term->colors)); term->color_stack.idx = slot - 1; /* Assume a full palette switch *will* affect almost all cells. The alternative is to call term_damage_color() for all 256 palette entries *and* the default fg/bg (256 + 2 calls in total) */ term_damage_view(term); term_damage_margins(term); } else if (slot == 0) { LOG_ERR("XTPOPCOLORS: cannot pop beyond the first element"); } else { LOG_ERR( "XTPOPCOLORS: invalid color slot: %d " "(stack has %zu slots, current slot is %zu)", vt_param_get(term, 0, 0), term->color_stack.size, term->color_stack.idx); } break; } case 'R': { /* XTREPORTCOLORS */ char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\033[?%zu;%zu#Q", term->color_stack.idx, term->color_stack.size); term_to_slave(term, reply, n); break; } } break; /* private[0] == '#' */ } case 0x243f: /* ?$ */ switch (final) { case 'p': { unsigned param = vt_param_get(term, 0, 0); /* * Request DEC private mode (DECRQM) * Reply: * 0 - not recognized * 1 - set * 2 - reset * 3 - permanently set * 4 - permantently reset */ unsigned status = decrqm(term, param); char reply[32]; size_t n = xsnprintf(reply, sizeof(reply), "\033[?%u;%u$y", param, status); term_to_slave(term, reply, n); break; } default: UNHANDLED(); break; } break; /* private[0] == '?' && private[1] == '$' */ default: UNHANDLED(); break; } } foot-1.21.0/csi.h000066400000000000000000000001631476600145200134760ustar00rootroot00000000000000#pragma once #include #include "terminal.h" void csi_dispatch(struct terminal *term, uint8_t final); foot-1.21.0/cursor-shape.c000066400000000000000000000124541476600145200153340ustar00rootroot00000000000000#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) { 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"}, }; for (size_t i = 0; i < ALEN(table); i++) { for (size_t j = 0; j < ALEN(table[i]); j++) { if (table[i][j] != NULL && streq(xcursor, table[i][j])) { return i; } } } return 0; } foot-1.21.0/cursor-shape.h000066400000000000000000000013371476600145200153370ustar00rootroot00000000000000#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); foot-1.21.0/dcs.c000066400000000000000000000335431476600145200134740ustar00rootroot00000000000000#include "dcs.h" #include #define LOG_MODULE "dcs" #define LOG_ENABLE_DBG 0 #include "log.h" #include "foot-terminfo.h" #include "sixel.h" #include "util.h" #include "vt.h" #include "xmalloc.h" #include "xsnprintf.h" static bool ensure_size(struct terminal *term, size_t required_size) { if (required_size <= term->vt.dcs.size) return true; uint8_t *new_data = realloc(term->vt.dcs.data, required_size); if (new_data == NULL) { LOG_ERRNO("failed to increase size of DCS buffer"); return false; } term->vt.dcs.data = new_data; term->vt.dcs.size = required_size; return true; } /* Decode hex-encoded string *inline*. NULL terminates */ static char * hex_decode(const char *s, size_t len) { if (len % 2) return NULL; char *hex = xmalloc(len / 2 + 1); char *o = hex; /* TODO: error checking */ for (size_t i = 0; i < len; i += 2) { uint8_t nib1 = hex2nibble(*s); s++; uint8_t nib2 = hex2nibble(*s); s++; if (nib1 == HEX_DIGIT_INVALID || nib2 == HEX_DIGIT_INVALID) goto err; *o = nib1 << 4 | nib2; o++; } *o = '\0'; return hex; err: free(hex); return NULL; } UNITTEST { /* Verify table is sorted */ const char *p = terminfo_capabilities; size_t left = sizeof(terminfo_capabilities); const char *last_cap = NULL; while (left > 0) { const char *cap = p; const char *val = cap + strlen(cap) + 1; size_t size = strlen(cap) + 1 + strlen(val) + 1;; xassert(size <= left); p += size; left -= size; if (last_cap != NULL) xassert(strcmp(last_cap, cap) < 0); last_cap = cap; } } static bool lookup_capability(const char *name, const char **value) { const char *p = terminfo_capabilities; size_t left = sizeof(terminfo_capabilities); while (left > 0) { const char *cap = p; const char *val = cap + strlen(cap) + 1; size_t size = strlen(cap) + 1 + strlen(val) + 1;; xassert(size <= left); p += size; left -= size; int r = strcmp(cap, name); if (r == 0) { *value = val; return true; } else if (r > 0) break; } *value = NULL; return false; } static void xtgettcap_reply(struct terminal *term, const char *hex_cap_name, size_t len) { char *name = hex_decode(hex_cap_name, len); if (name == NULL) { LOG_WARN("XTGETTCAP: invalid hex encoding, ignoring capability"); return; } const char *value; bool valid_capability = lookup_capability(name, &value); xassert(!valid_capability || value != NULL); LOG_DBG("XTGETTCAP: cap=%s (%.*s), value=%s", name, (int)len, hex_cap_name, valid_capability ? value : ""); if (!valid_capability) goto err; if (value[0] == '\0') { /* Boolean */ term_to_slave(term, "\033P1+r", 5); term_to_slave(term, hex_cap_name, len); term_to_slave(term, "\033\\", 2); goto out; } /* * Reply format: * \EP 1 + r cap=value \E\\ * Where 'cap' and 'value are hex encoded ascii strings */ char *reply = xmalloc( 5 + /* DCS 1 + r (\EP1+r) */ len + /* capability name, hex encoded */ 1 + /* '=' */ strlen(value) * 2 + /* capability value, hex encoded */ 2 + /* ST (\E\\) */ 1); int idx = sprintf(reply, "\033P1+r%.*s=", (int)len, hex_cap_name); for (const char *c = value; *c != '\0'; c++) { uint8_t nib1 = (uint8_t)*c >> 4; uint8_t nib2 = (uint8_t)*c & 0xf; reply[idx] = nib1 >= 0xa ? 'A' + nib1 - 0xa : '0' + nib1; idx++; reply[idx] = nib2 >= 0xa ? 'A' + nib2 - 0xa : '0' + nib2; idx++; } reply[idx] = '\033'; idx++; reply[idx] = '\\'; idx++; term_to_slave(term, reply, idx); free(reply); goto out; err: term_to_slave(term, "\033P0+r", 5); term_to_slave(term, hex_cap_name, len); term_to_slave(term, "\033\\", 2); out: free(name); } static void xtgettcap_put(struct terminal *term, uint8_t c) { struct vt *vt = &term->vt; /* Grow buffer expontentially */ if (vt->dcs.idx >= vt->dcs.size) { size_t new_size = vt->dcs.size * 2; if (new_size == 0) new_size = 128; if (!ensure_size(term, new_size)) return; } vt->dcs.data[vt->dcs.idx++] = c; } static void xtgettcap_unhook(struct terminal *term) { size_t left = term->vt.dcs.idx; const char *const end = (const char *)&term->vt.dcs.data[left]; const char *p = (const char *)term->vt.dcs.data; if (p == NULL) { /* Request is empty; send an error reply, without any capabilities */ term_to_slave(term, "\033P0+r\033\\", 7); return; } while (true) { const char *sep = memchr(p, ';', left); size_t cap_len; if (sep == NULL) { /* Last capability */ cap_len = end - p; } else { cap_len = sep - p; } xtgettcap_reply(term, p, cap_len); left -= cap_len + 1; p += cap_len + 1; if (sep == NULL) break; } } static void NOINLINE append_sgr_attr_n(char **reply, size_t *len, const char *attr, size_t n) { size_t new_len = *len + n + 1; *reply = xrealloc(*reply, new_len); memcpy(&(*reply)[*len], attr, n); (*reply)[new_len - 1] = ';'; *len = new_len; } static void decrqss_put(struct terminal *term, uint8_t c) { /* Largest request we support is two bytes */ if (!ensure_size(term, 2)) return; struct vt *vt = &term->vt; if (vt->dcs.idx >= 2) return; vt->dcs.data[vt->dcs.idx++] = c; } static void decrqss_unhook(struct terminal *term) { const uint8_t *query = term->vt.dcs.data; const size_t n = term->vt.dcs.idx; /* * A note on the Ps parameter in the reply: many DEC manual * instances (e.g. https://vt100.net/docs/vt510-rm/DECRPSS) claim * that 0 means "request is valid", and 1 means "request is * invalid". * * However, this appears to be a typo; actual hardware inverts the * response (as does XTerm and mlterm): * https://github.com/hackerb9/vt340test/issues/13 */ if (n == 1 && query[0] == 'r') { /* DECSTBM - Set Top and Bottom Margins */ char reply[64]; size_t len = xsnprintf(reply, sizeof(reply), "\033P1$r%d;%dr\033\\", term->scroll_region.start + 1, term->scroll_region.end); term_to_slave(term, reply, len); } else if (n == 1 && query[0] == 'm') { /* SGR - Set Graphic Rendition */ char *reply = NULL; size_t len = 0; #define append_sgr_attr(num_as_str) \ append_sgr_attr_n(&reply, &len, num_as_str, sizeof(num_as_str) - 1) /* Always present, both in the example from the VT510 manual * (https://vt100.net/docs/vt510-rm/DECRPSS), and in XTerm and * mlterm */ append_sgr_attr("0"); struct attributes *a = &term->vt.attrs; if (a->bold) append_sgr_attr("1"); if (a->dim) append_sgr_attr("2"); if (a->italic) append_sgr_attr("3"); if (a->underline) { if (term->vt.underline.style > UNDERLINE_SINGLE) { char value[4]; size_t val_len = xsnprintf(value, sizeof(value), "4:%d", term->vt.underline.style); append_sgr_attr_n(&reply, &len, value, val_len); } else append_sgr_attr("4"); } if (a->blink) append_sgr_attr("5"); if (a->reverse) append_sgr_attr("7"); if (a->conceal) append_sgr_attr("8"); if (a->strikethrough) append_sgr_attr("9"); switch (a->fg_src) { case COLOR_DEFAULT: break; case COLOR_BASE16: { char value[4]; size_t val_len = xsnprintf( value, sizeof(value), "%u", a->fg >= 8 ? a->fg - 8 + 90 : a->fg + 30); append_sgr_attr_n(&reply, &len, value, val_len); break; } case COLOR_BASE256: { char value[16]; size_t val_len = xsnprintf(value, sizeof(value), "38:5:%u", a->fg); append_sgr_attr_n(&reply, &len, value, val_len); break; } case COLOR_RGB: { uint8_t r = a->fg >> 16; uint8_t g = a->fg >> 8; uint8_t b = a->fg >> 0; char value[32]; size_t val_len = xsnprintf( value, sizeof(value), "38:2::%hhu:%hhu:%hhu", r, g, b); append_sgr_attr_n(&reply, &len, value, val_len); break; } } switch (a->bg_src) { case COLOR_DEFAULT: break; case COLOR_BASE16: { char value[4]; size_t val_len = xsnprintf( value, sizeof(value), "%u", a->bg >= 8 ? a->bg - 8 + 100 : a->bg + 40); append_sgr_attr_n(&reply, &len, value, val_len); break; } case COLOR_BASE256: { char value[16]; size_t val_len = xsnprintf(value, sizeof(value), "48:5:%u", a->bg); append_sgr_attr_n(&reply, &len, value, val_len); break; } case COLOR_RGB: { uint8_t r = a->bg >> 16; uint8_t g = a->bg >> 8; uint8_t b = a->bg >> 0; char value[32]; size_t val_len = xsnprintf( value, sizeof(value), "48:2::%hhu:%hhu:%hhu", r, g, b); append_sgr_attr_n(&reply, &len, value, val_len); break; } } switch (term->vt.underline.color_src) { case COLOR_DEFAULT: case COLOR_BASE16: break; case COLOR_BASE256: { char value[16]; size_t val_len = xsnprintf( value, sizeof(value), "58:5:%u", term->vt.underline.color); append_sgr_attr_n(&reply, &len, value, val_len); break; } case COLOR_RGB: { uint8_t r = term->vt.underline.color >> 16; uint8_t g = term->vt.underline.color >> 8; uint8_t b = term->vt.underline.color >> 0; char value[32]; size_t val_len = xsnprintf( value, sizeof(value), "58:2::%hhu:%hhu:%hhu", r, g, b); append_sgr_attr_n(&reply, &len, value, val_len); break; } } #undef append_sgr_attr_n reply[len - 1] = 'm'; term_to_slave(term, "\033P1$r", 5); term_to_slave(term, reply, len); term_to_slave(term, "\033\\", 2); free(reply); } else if (n == 2 && memcmp(query, " q", 2) == 0) { /* DECSCUSR - Set Cursor Style */ int mode; switch (term->cursor_style) { case CURSOR_HOLLOW: /* FALLTHROUGH */ case CURSOR_BLOCK: mode = 2; break; case CURSOR_UNDERLINE: mode = 4; break; case CURSOR_BEAM: mode = 6; break; default: BUG("invalid cursor style"); break; } if (term->cursor_blink.deccsusr) mode--; char reply[16]; size_t len = xsnprintf(reply, sizeof(reply), "\033P1$r%d q\033\\", mode); term_to_slave(term, reply, len); } else { static const char err[] = "\033P0$r\033\\"; term_to_slave(term, err, sizeof(err) - 1); } } void dcs_hook(struct terminal *term, uint8_t final) { LOG_DBG("hook: %c (intermediate(s): %.2s, param=%d)", final, (const char *)&term->vt.private, vt_param_get(term, 0, 0)); xassert(term->vt.dcs.data == NULL); xassert(term->vt.dcs.size == 0); xassert(term->vt.dcs.put_handler == NULL); xassert(term->vt.dcs.unhook_handler == NULL); switch (term->vt.private) { case 0: switch (final) { case 'q': { if (!term->conf->tweak.sixel) { break; } int p1 = vt_param_get(term, 0, 0); int p2 = vt_param_get(term, 1, 0); int p3 = vt_param_get(term, 2, 0); term->vt.dcs.put_handler = sixel_init(term, p1, p2, p3); term->vt.dcs.unhook_handler = &sixel_unhook; break; } } break; case '$': switch (final) { case 'q': term->vt.dcs.put_handler = &decrqss_put; term->vt.dcs.unhook_handler = &decrqss_unhook; break; } break; case '=': switch (final) { case 's': /* BSU/ESU: https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec */ switch (vt_param_get(term, 0, 0)) { case 1: term->vt.dcs.unhook_handler = &term_enable_app_sync_updates; return; case 2: term->vt.dcs.unhook_handler = &term_disable_app_sync_updates; return; } break; } break; case '+': switch (final) { case 'q': /* XTGETTCAP */ term->vt.dcs.put_handler = &xtgettcap_put; term->vt.dcs.unhook_handler = &xtgettcap_unhook; break; } break; } } void dcs_put(struct terminal *term, uint8_t c) { /* LOG_DBG("PUT: %c", c); */ if (term->vt.dcs.put_handler != NULL) term->vt.dcs.put_handler(term, c); } void dcs_unhook(struct terminal *term) { if (term->vt.dcs.unhook_handler != NULL) term->vt.dcs.unhook_handler(term); term->vt.dcs.unhook_handler = NULL; term->vt.dcs.put_handler = NULL; free(term->vt.dcs.data); term->vt.dcs.data = NULL; term->vt.dcs.size = 0; term->vt.dcs.idx = 0; } foot-1.21.0/dcs.h000066400000000000000000000003071476600145200134710ustar00rootroot00000000000000#pragma once #include #include "terminal.h" void dcs_hook(struct terminal *term, uint8_t final); void dcs_put(struct terminal *term, uint8_t c); void dcs_unhook(struct terminal *term); foot-1.21.0/debug.c000066400000000000000000000020271476600145200140020ustar00rootroot00000000000000#include "debug.h" #include #include #include #include #include #include "log.h" #if defined(__SANITIZE_ADDRESS__) || HAS_FEATURE(address_sanitizer) #include #define ASAN_ENABLED 1 #endif static void print_stack_trace(void) { #ifdef ASAN_ENABLED fputs("\nStack trace:\n", stderr); __sanitizer_print_stack_trace(); #endif } noreturn void fatal_error(const char *file, int line, const char *msg, int err) { log_msg(LOG_CLASS_ERROR, "debug", file, line, "%s: %s", msg, strerror(err)); print_stack_trace(); fflush(stderr); abort(); } noreturn void bug(const char *file, int line, const char *func, const char *fmt, ...) { char buf[4096]; va_list ap; va_start(ap, fmt); int n = vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); const char *msg = likely(n >= 0) ? buf : "??"; log_msg(LOG_CLASS_ERROR, "debug", file, line, "BUG in %s(): %s", func, msg); print_stack_trace(); fflush(stderr); abort(); } foot-1.21.0/debug.h000066400000000000000000000016611476600145200140120ustar00rootroot00000000000000#pragma once #include "macros.h" #define FATAL_ERROR(...) fatal_error(__FILE__, __LINE__, __VA_ARGS__) #ifdef NDEBUG #define BUG(...) UNREACHABLE() #else #define BUG(...) bug(__FILE__, __LINE__, __func__, __VA_ARGS__) #endif #define xassert(x) do { \ IGNORE_WARNING("-Wtautological-compare") \ if (unlikely(!(x))) { \ BUG("assertion failed: '%s'", #x); \ } \ UNIGNORE_WARNINGS \ } while (0) #ifndef static_assert #if __STDC_VERSION__ >= 201112L #define static_assert(x, msg) _Static_assert((x), msg) #elif GNUC_AT_LEAST(4, 6) || HAS_EXTENSION(c_static_assert) #define static_assert(x, msg) __extension__ _Static_assert((x), msg) #else #define static_assert(x, msg) #endif #endif noreturn void fatal_error(const char *file, int line, const char *msg, int err) COLD; noreturn void bug(const char *file, int line, const char *func, const char *fmt, ...) PRINTF(4) COLD; foot-1.21.0/doc/000077500000000000000000000000001476600145200133145ustar00rootroot00000000000000foot-1.21.0/doc/benchmark.md000066400000000000000000000064621476600145200156000ustar00rootroot00000000000000# Benchmarks ## vtebench All benchmarks are done using [vtebench](https://github.com/alacritty/vtebench): ```sh ./target/release/vtebench -b ./benchmarks --dat /tmp/ ``` ## 2022-05-12 ### System CPU: i9-9900 RAM: 64GB Graphics: Radeon RX 5500XT ### Terminal configuration Geometry: 2040x1884 Font: Fantasque Sans Mono 10.00pt/23px Scrollback: 10000 lines ### Results | Benchmark (times in ms) | Foot (GCC+PGO) 1.12.1 | Foot 1.12.1 | Alacritty 0.10.1 | URxvt 9.26 | XTerm 372 | |-------------------------------|----------------------:|------------:|-----------------:|-----------:|----------:| | cursor motion | 10.40 | 14.07 | 24.97 | 23.38 | 1622.86 | | dense cells | 29.58 | 45.46 | 97.45 | 10828.00 | 2323.00 | | light cells | 4.34 | 4.40 | 12.84 | 12.17 | 49.81 | | scrollling | 135.31 | 116.35 | 121.69 | 108.30 | 4041.33 | | scrolling bottom region | 118.19 | 109.70 | 105.26 | 118.80 | 3875.00 | | scrolling bottom small region | 132.41 | 122.11 | 122.83 | 151.30 | 3839.67 | | scrolling fullscreen | 5.70 | 5.66 | 10.92 | 12.09 | 124.25 | | scrolling top region | 144.19 | 121.78 | 135.81 | 159.24 | 3858.33 | | scrolling top small region | 135.95 | 119.01 | 115.46 | 216.55 | 3872.67 | | unicode | 11.56 | 10.92 | 15.94 | 1012.27 | 4779.33 | ## 2022-05-12 ### System CPU: i5-8250U RAM: 8GB Graphics: Intel UHD Graphics 620 ### Terminal configuration Geometry: 945x1020 Font: Dina:pixelsize=12 Scrollback=10000 lines ### Results | Benchmark (times in ms) | Foot (GCC+PGO) 1.12.1 | Foot 1.12.1 | Alacritty 0.10.1 | URxvt 9.26 | XTerm 372 | |-------------------------------|----------------------:|------------:|-----------------:|-----------:|----------:| | cursor motion | 15.03 | 16.74 | 23.22 | 24.14 | 1381.63 | | dense cells | 43.56 | 54.10 | 89.43 | 1807.17 | 1945.50 | | light cells | 7.96 | 9.66 | 20.19 | 21.31 | 122.44 | | scrollling | 146.02 | 150.47 | 129.22 | 129.84 | 10140.00 | | scrolling bottom region | 138.36 | 137.42 | 117.06 | 141.87 | 10136.00 | | scrolling bottom small region | 137.40 | 134.66 | 128.97 | 208.77 | 9930.00 | | scrolling fullscreen | 11.66 | 12.02 | 19.69 | 21.96 | 315.80 | | scrolling top region | 143.81 | 133.47 | 132.51 | 475.81 | 10267.00 | | scrolling top small region | 133.72 | 135.32 | 145.10 | 314.13 | 10074.00 | | unicode | 20.89 | 21.78 | 26.11 | 5687.00 | 15740.00 | foot-1.21.0/doc/foot-ctlseqs.7.scd000066400000000000000000000451251476600145200166060ustar00rootroot00000000000000foot-ctlseqs(7) # NAME foot-ctlseqs - terminal control sequences supported by foot # DESCRIPTION This document describes all the control sequences supported by foot. - Control characters - Sequences beginning with ESC - CSI - Control Sequence Introducer - SGR - Indexed and RGB colors (256-color palette and 24-bit colors) - Private modes - Window manipulation - Other - OSC - Operating System Command - DCS - Device Control String # Control characters [[ *Sequence* :[ *Name* :< *Description* | \\a : BEL : Depends on what *bell* in *foot.ini*(5) is set to. | \\b : BS : Backspace; move the cursor left one step. Wrap if _bw_ is enabled. | \\t : HT : Horizontal tab; move the cursor to the next tab stop. | \\n : LF : Line feed; move the cursor down one step, or scroll content up if at the bottom line. | \\v : VT : Vertical tab; identical to _LF_. | \\f : FF : Form feed; identical to _LF_. | \\r : CR : Carriage ret; move the cursor to the leftmost column. | \\x0E : SO : Shift out; select the _G1_ character set. | \\x0F : SI : Shift in; select the _G0_ character set. # Sequences beginning with ESC Note: this table excludes sequences where ESC is part of a 7-bit equivalent to 8-bit C1 controls. [[ *Sequence* :[ *Name* :[ *Origin* :< *Description* | \\E 7 : DECSC : VT100 : Save cursor position. | \\E 8 : DECRC : VT100 : Restore cursor position. | \\E c : RIS : VT100 : Reset terminal to initial state. | \\E D : IND : VT100 : Line feed; move the cursor down one step, or scroll content up if at the bottom margin. | \\E E : NEL : VT100 : Next line; move the cursor down one step, and to the first column. Content is scrolled up if at the bottom line. | \\E H : HTS : VT100 : Set one horizontal tab stop at the current position. | \\E M : RI : VT100 : Reverse index; move the cursor up one step, or scroll content down if at the top margin. | \\E N : SS2 : VT220 : Single shift select of G2 character set (affects next character only). | \\E O : SS3 : VT220 : Single shift select of G3 character set (affects next character only). | \\E = : DECKPAM : VT100 : Switch keypad to _application_ mode. | \\E > : DECKPNM : VT100 : Switch keypad to _numeric_ mode. | \\E ( _C_ : SCS : VT100 : Designate G0 character set. Supported values for _C_ are: *0* (DEC Special Character and Line Drawing Set), and *B* (USASCII). | \\E ) _C_ : SCS : VT100 : Designate G1 character set. Same supported values for _C_ as in _G0_. | \\E \* _C_ : SCS : VT220 : Designate G2 character set. Same supported values for _C_ as in _G0_. | \\E + _C_ : SCS : VT220 : Designate G3 character set. Same supported values for _C_ as in _G0_. # CSI All sequences begin with *\\E[*, sometimes abbreviated "CSI". Spaces are used in the sequence strings to make them easier to read, but are not actually part of the string (i.e. *\\E[ 1 m* is really *\\E[1m*). ## SGR All SGR sequences are in the form *\\E[* _N_ *m*, where _N_ is a decimal number - the _parameter_. Multiple parameters can be combined in a single CSI sequence by separating them with semicolons: *\\E[ 1;2;3 m*. [[ *Parameter* :< *Description* | 0 : Reset all attributes | 1 : Bold | 2 : Dim | 3 : Italic | 4 : Underline, including styled underlines | 5 : Blink | 7 : Reverse video; swap foreground and background colors | 8 : Conceal; text is not visible, but is copiable | 9 : Crossed-out/strike | 21 : Double underline | 22 : Disable *bold* and *dim* | 23 : Disable italic | 24 : Disable underline | 25 : Disable blink | 27 : Disable reverse video | 28 : Disable conceal | 29 : Disable crossed-out | 30-37 : Select foreground color (using *regularN* in *foot.ini*(5)) | 38 : Select foreground color, see "indexed and RGB colors" below | 39 : Use the default foreground color (*foreground* in *foot.ini*(5)) | 40-47 : Select background color (using *regularN* in *foot.ini*(5)) | 48 : Select background color, see "indexed and RGB colors" below | 49 : Use the default background color (*background* in *foot.ini*(5)) | 58 : Select underline color, see "indexed and RGB colors" below | 59 : Use the default underline color | 90-97 : Select foreground color (using *brightN* in *foot.ini*(5)) | 100-107 : Select background color (using *brightN* in *foot.ini*(5)) ## Indexed and RGB colors (256-color palette and 24-bit colors) Foot supports both the new sub-parameter based variants, and the older parameter based variants for setting foreground and background colors. Indexed colors: - *\\E[ 38 : 5 :* _idx_ *m* - *\\E[ 38 ; 5 ;* _idx_ *m* RGB colors: - *\\E[ 38 : 2 :* _cs_ *:* _r_ *:* _g_ *:* _b_ *m* - *\\E[ 38 : 2 :* _r_ *:* _g_ *:* _b_ *m* - *\\E[ 38 ; 2 ;* _r_ *;* _g_ *;* _b_ *m* The first variant is the "correct" one (and foot also recognizes, but ignores, the optional _tolerance_ parameters). The second one is allowed since many programs "forget" the color space ID, _cs_. The sub-parameter based variants are preferred, and are what foot's *terminfo*(5) entry uses. ## Private Modes There are several Boolean-like "modes" that affect certain aspects of the terminal's behavior. These modes can be manipulated with the following 4 escape sequences: [[ *Sequence* :[ *Name* :< *Description* | \\E[ ? _Pm_ h : DECSET : Enable private mode | \\E[ ? _Pm_ l : DECRST : Disable private mode | \\E[ ? _Pm_ s : XTSAVE : Save private mode | \\E[ ? _Pm_ r : XTRESTORE : Restore private mode The _Pm_ parameter in the above sequences denotes a numerical ID that corresponds to one of the following modes: [[ *Parameter* :[ *Origin* :< *Description* | 1 : VT100 : Cursor keys mode (DECCKM) | 5 : VT100 : Reverse video (DECSCNM) | 6 : VT100 : Origin mode (DECOM) | 7 : VT100 : Auto-wrap mode (DECAWM) | 12 : AT&T 610 : Cursor blink | 25 : VT220 : Cursor visibility (DECTCEM) | 45 : xterm : Reverse-wraparound mode | 47 : xterm : Same as 1047 (see below) | 66 : VT320 : Numeric keypad mode (DECNKM); same as DECKPAM/DECKPNM when enabled/disabled | 1000 : xterm : Send mouse x/y on button press/release | 1001 : xterm : Use hilite mouse tracking | 1002 : xterm : Use cell motion mouse tracking | 1003 : xterm : Use all motion mouse tracking | 1004 : xterm : Send FocusIn/FocusOut events | 1006 : xterm : SGR mouse mode | 1007 : xterm : Alternate scroll mode | 1015 : urxvt : urxvt mouse mode | 1016 : xterm : SGR-Pixels mouse mode | 1034 : xterm : 8-bit Meta mode | 1035 : xterm : Num Lock modifier (see xterm numLock option) | 1036 : xterm : Send ESC when Meta modifies a key (see xterm metaSendsEscape option) | 1042 : xterm : Perform action for BEL character (see *bell* in *foot.ini*(5)) | 1047 : xterm : Use alternate screen buffer | 1048 : xterm : Save/restore cursor (DECSET=save, DECRST=restore) | 1049 : xterm : Equivalent to 1048 and 1047 combined | 1070 : xterm : Use private color registers for each sixel | 2004 : xterm : Wrap pasted text with start/end delimiters (bracketed paste mode) | 2026 : terminal-wg : Application synchronized updates mode | 2027 : contour : Grapheme cluster processing | 2048 : TODO : In-band window resize notifications | 8452 : xterm : Position cursor to the right of sixels, instead of on the next line | 737769 : foot : Input Method Editor (IME) mode ## Window manipulation Foot implements a sub-set of XTerm's (originally dtterm's) window manipulation sequences. The generic format is: *\\E[ *_Ps_* ; *_Ps_* ; *_Ps_* t* [[ *Parameter 1* :[ *Parameter 2* :< *Description* | 11 : - : Report if window is iconified. Foot always reports *1* - not iconified. | 13 : - : Report window position. Foot always reports (0,0), due to Wayland limitations. | 13 : 2 : Report text area position. Foot always reports (0,0) due to Wayland limitations. | 14 : - : Report text area size, in pixels. Foot reports the grid size, excluding the margins. | 14 : 2 : Report window size, in pixels. Foot reports the grid size plus the margins. | 15 : - : Report the screen size, in pixels. | 16 : - : Report the cell size, in pixels. | 18 : - : Report text area size, in characters. | 19 : - : Report screen size, in characters. | 20 : - : Report icon label. | 22 : - : Push window title+icon. | 22 : 1 : Push window icon. | 22 : 2 : Push window title. | 23 : - : Pop window title+icon. | 23 : 1 : Pop window icon. | 23 : 2 : Pop window title. ## Other [[ *Parameter* :[ *Name* :[ *Origin* :< *Description* | \\E[ _Ps_ c : DA : VT100 : Send primary device attributes. Foot responds with "I'm a VT220 with sixel and ANSI color support". | \\E[ _Ps_ A : CUU : VT100 : Cursor up - move cursor up _Ps_ times. | \\E[ _Ps_ B : CUD : VT100 : Cursor down - move cursor down _Ps_ times. | \\E[ _Ps_ C : CUF : VT100 : Cursor forward - move cursor to the right _Ps_ times. | \\E[ _Ps_ D : CUB : VT100 : Cursor backward - move cursor to the left _Ps_ times. | \\E[ _Ps_ g : TBC : VT100 : Tab clear. _Ps_=0 -> clear current column. _Ps_=3 -> clear all. | \\E[ _Ps_ ; _Ps_ f : HVP : VT100 : Horizontal and vertical position - move cursor to _row_ ; _column_. | \\E[ _Ps_ ; _Ps_ H : CUP : VT100 : Cursor position - move cursor to _row_ ; _column_. | \\E[ _Ps_ J : ED : VT100 : Erase in display. _Ps_=0 -> below cursor. _Ps_=1 -> above | \\E[ _Ps_ K : EL : VT100 : Erase in line. _Ps_=0 -> right of cursor. _Ps_=1 -> left of cursor. _Ps_=2 -> all. | \\E[ _Pm_ h : SM : VT100 : Set mode. _Pm_=4 -> enable IRM (Insertion Replacement Mode). All other values of _Pm_ are unsupported. | \\E[ _Pm_ l : RM : VT100 : Reset mode. _Pm_=4 -> disable IRM (Insertion Replacement Mode). All other values of _Pm_ are unsupported. | \\E[ _Ps_ n : DSR : VT100 : Device status report. _Ps_=5 -> device status. _Ps_=6 -> cursor position. | \\E[ _Ps_ L : IL : VT220 : Insert _Ps_ lines. | \\E[ _Ps_ M : DL : VT220 : Delete _Ps_ lines. | \\E[ _Ps_ P : DCH : VT220 : Delete _Ps_ characters. | \\E[ _Ps_ @ : ICH : VT220 : Insert _Ps_ blank characters. | \\E[ _Ps_ X : ECH : VT220 : Erase _Ps_ characters. | \\E[ > c : DA2 : VT220 : Send secondary device attributes. Foot responds with "I'm a VT220 and here's my version number". | \\E[ ! p : DECSTR : VT220 : Soft terminal reset. | \\E[ ? _Ps_ $ p : DECRQM : VT320 : Request status of DEC private mode. The _Ps_ parameter corresponds to one of the values mentioned in the "Private Modes" section above (as set with DECSET/DECRST). | \\E[ _Ps_ $ p : DECRQM : VT320 : Request status of ECMA-48/ANSI mode. See the descriptions for SM/RM above for recognized _Ps_ values. | \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pm_ $ r : DECCARA : VT400 : Change attributes in rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ denotes the rectangle, _Pm_ denotes the SGR attributes. | \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pm_ $ t : DECRARA : VT400 : Invert attributes in rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ denotes the rectangle, _Pm_ denotes the SGR attributes. | \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pp_ ; _Pt_ ; _Pl_ ; _Pp_ $ v : DECCRA : VT400 : Copy rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ denotes the rectangle, _Pt_ and _Pl_ denotes the target location. | \\E[ _Pc_ ; _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ $ x : DECFRA : VT420 : Fill rectangular area. _Pc_ is the character to use, _Pt_, _Pl_, _Pb_ and _Pr_ denotes the rectangle. | \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ $ z : DECERA : VT400 : Erase rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ denotes the rectangle. | \\E[ _Ps_ T : SD : VT420 : Scroll down _Ps_ lines. | \\E[ s : SCOSC : SCO, VT510 : Save cursor position. | \\E[ u : SCORC : SCO, VT510 : Restore cursor position. | \\E[ _Ps_ SP q : DECSCUSR : VT510 : Set cursor style. In foot, _Ps_=0 means "use style from foot.ini". | \\E[ = _Ps_ c : DA3 : VT510 : Send tertiary device attributes. Foot responds with "FOOT", in hexadecimal. | \\E[ _Pm_ d : VPA : ECMA-48 : Line position absolute - move cursor to line _Pm_. | \\E[ _Pm_ e : VPR : ECMA-48 : Line position relative - move cursor down _Pm_ lines. | \\E[ _Pm_ a : HPR : ECMA-48 : Character position relative - move cursor to the right _Pm_ times. | \\E[ _Ps_ E : CNL : ECMA-48 : Cursor next line - move the cursor down _Ps_ times. | \\E[ _Ps_ F : CPL : ECMA-48 : Cursor preceding line - move the cursor up _Ps_ times. | \\E[ _Pm_ ` : HPA : ECMA-48 : Character position absolute - move cursor to column _Pm_. | \\E[ _Ps_ G : CHA : ECMA-48 : Cursor character absolute - move cursor to column _Ps_. cursor. _Ps_=2 -> all. _Ps_=3 -> saved lines. | \\E[ _Ps_ S : SU : ECMA-48 : Scroll up _Ps_ lines. | \\E[ _Ps_ I : CHT : ECMA-48 : Cursor forward tabulation _Ps_ tab stops. | \\E[ _Ps_ Z : CBT : ECMA-48 : Cursor backward tabulation _Ps_ tab stops. | \\E[ _Ps_ b : REP : ECMA-48 : Repeat the preceding printable character _Ps_ times. | \\E[ ? _Pi_ ; _Pa_ ; _Pv_ S : XTSMGRAPHICS : xterm : Set or request sixel attributes. | \\E[ > _Ps_ q : XTVERSION : xterm : _Ps_=0 -> report terminal name and version, in the form *\\EP>|foot(version)\\E\\*. | \\E[ > 4 ; _Pv_ m : XTMODKEYS : xterm : Set level of the _modifyOtherKeys_ property to _Pv_. Note that foot only supports level 1 and 2, where level 1 is the default setting. | \\E[ ? _Pp_ m : XTQMODKEYS : xterm : Query key modifier options | \\E[ > 4 n : : xterm : Resets the _modifyOtherKeys_ property to level 1. Note that in foot, this sequence does not completely disable _modifyOtherKeys_, since foot only supports level 1 and level 2 (and not level 0). | \\E[ ? u : : kitty : Query current values of the Kitty keyboard flags. | \\E[ > _flags_ u : : kitty : Push a new entry, _flags_, to the Kitty keyboard stack. | \\E[ < _number_ u : : kitty : Pop _number_ of entries from the Kitty keyboard stack. | \\E[ = _flags_ ; _mode_ u : : kitty : Update current Kitty keyboard flags, according to _mode_. | \\E[ # P : XTPUSHCOLORS : xterm : Push current color palette onto stack | \\E[ # Q : XTPOPCOLORS : xterm : Pop color palette from stack | \\E[ # R : XTREPORTCOLORS : xterm : Report the current entry on the palette stack, and the number of palettes stored on the stack. # OSC All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. [[ *Sequence* :[ *Origin* :< *Description* | \\E] 0 ; _Pt_ \\E\\ : xterm : Set window icon and title to _Pt_. | \\E] 1 ; _Pt_ \\E\\ : xterm : Set window icon to _Pt_. | \\E] 2 ; _Pt_ \\E\\ : xterm : Set window title to _Pt_ | \\E] 4 ; _c_ ; _spec_ \\E\\ : xterm : Change color number _c_ to _spec_, where _spec_ is a color in XParseColor format. foot only supports RGB colors; either *rgb://*, or the legacy format (*#rgb*). | \\E] 7 ; _Uri_ \\E\\ : iTerm2 : Update the terminal's current working directory. Newly spawned terminals will launch in this directory. _Uri_ must be in the format *file:///*. *hostname* must refer to your local host. | \\E] 8 ; id=_ID_ ; _Uri_ \\E\\ : VTE+iTerm2 : Hyperlink (a.k.a HTML-like anchors). id=_ID_ is optional; if assigned, all URIs with the same _ID_ will be treated as a single hyperlink. An empty URI closes the hyperlink. | \\E] 9 ; _msg_ \\E\\ : iTerm2 : Desktop notification, uses *notify* in *foot.ini*(5). | \\E] 10 ; _spec_ \\E\\ : xterm : Change the default foreground color to _spec_, a color in XParseColor format. | \\E] 11 ; _spec_ \\E\\ : xterm : Change the default background color to _spec_, a color in XParseColor format. Foot implements URxvt's transparency extension; e.g. _spec_=*[75]#ff00ff* or _spec_=*rgba:ff/00/ff/bf* (pink with 75% alpha). | \\E] 12 ; _spec_ \\E\\ : xterm : Change cursor color to _spec_, a color in XParseColor format. | \\E] 17 ; _spec_ \\E\\ : xterm : Change selection background color to _spec_, a color in XParseColor format. | \\E] 19 ; _spec_ \\E\\ : xterm : Change selection foreground color to _spec_, a color in XParseColor format. | \\E] 22 ; _xcursor-pointer-name_ \\E\\ : xterm : Sets the xcursor pointer. An empty name, or an invalid name resets it. | \\E] 52 ; _Pc_ ; ? \\E\\ : xterm : Send clipboard data. _Pc_ can be either *c*, *s* or *p*. *c* uses the clipboard as source, and *s* and *p* uses the primary selection. The response is *\\E] 52 ; Pc ; \E\\*, where _Pc_ denotes the source used. | \\E] 52 ; _Pc_ ; _Pd_ \\E\\ : xterm : Copy _Pd_ (base64 encoded text) to the clipboard. _Pc_ denotes the target: *c* targets the clipboard and *s* and *p* the primary selection. | \\E] 66 ; _params_ ; text \\E\\ : kitty : Text sizing protocol (only 'w', width, supported) | \\E] 99 ; _params_ ; _payload_ \\E\\ : kitty : Desktop notification; uses *desktop-notifications.command* in *foot.ini*(5). | \\E] 104 ; _c_ \\E\\ : xterm : Reset color number _c_ (multiple semicolon separated _c_ values may be provided), or all colors (excluding the default foreground/background colors) if _c_ is omitted. | \\E] 110 \\E\\ : xterm : Reset default foreground color | \\E] 111 \\E\\ : xterm : Reset default background color | \\E] 112 \\E\\ : xterm : Reset cursor color | \\E] 117 \\E\\ : xterm : Reset selection background color | \\E] 119 \\E\\ : xterm : Reset selection foreground color | \\E] 133 ; A \\E\\ : FinalTerm : Mark start of shell prompt | \\E] 133 ; C \\E\\ : FinalTerm : Mark start of command output | \\E] 133 ; D \\E\\ : FinalTerm : Mark end of command output | \\E] 176 ; _app-id_ \\E\\ : foot : Set app ID. _app-id_ is optional; if assigned, the terminal window App ID will be set to the value. An empty App ID resets the value to the default. | \\E] 555 \\E\\ : foot : Flash the entire terminal (foot extension) | \\E] 777;notify;_title_;_msg_ \\E\\ : urxvt : Desktop notification, uses *desktop-notifications.command* in *foot.ini*(5). # DCS All _DCS_ sequences begin with *\\EP* (sometimes abbreviated _DCS_), and are terminated by *\\E\\* (ST). [[ *Sequence* :< *Description* | \\EP q \\E\\ : Emit a sixel image at the current cursor position | \\EP $ q \\E\\ : Request selection or setting (DECRQSS). Implemented queries: DECSTBM, SGR and DECSCUSR. | \\EP = _C_ s \\E\\ : Begin (_C_=*1*) or end (_C_=*2*) application synchronized updates. This sequence is supported for compatibility reasons, but it's recommended to use private mode 2026 (see above) instead. | \\EP + q \\E\\ : Query builtin terminfo database (XTGETTCAP) # FOOTNOTE Foot does not support 8-bit control characters ("C1"). foot-1.21.0/doc/foot.1.scd000066400000000000000000000527321476600145200151260ustar00rootroot00000000000000foot(1) # NAME foot - Wayland terminal emulator # SYNOPSIS *foot* [_OPTIONS_]++ *foot* [_OPTIONS_] <_command_> [_COMMAND OPTIONS_] All trailing (non-option) arguments are treated as a command, and its arguments, to execute (instead of the default shell). # DESCRIPTION *foot* is a Wayland terminal emulator. Running it without arguments will start a new terminal window with your default shell. You can override the default shell by appending a custom command to the foot command line *foot htop* # OPTIONS *-c*,*--config*=_PATH_ Path to configuration file, see *foot.ini*(5) for details. *-C*,*--check-config* Verify configuration and then exit with 0 if ok, otherwise exit with 230 (see *EXIT STATUS*). *-o*,*--override*=[_SECTION_.]_KEY_=_VALUE_ Override an option set in the configuration file. If _SECTION_ is not given, defaults to _main_. *-f*,*--font*=_FONT_ Comma separated list of fonts to use, in fontconfig format (see *FONT FORMAT*). The first font is the primary font. The remaining fonts are fallback fonts that will be used whenever a glyph cannot be found in the primary font. The fallback fonts are searched in the order they appear. If a glyph cannot be found in any of the fallback fonts, the dynamic fallback list from fontconfig (for the primary font) is searched. Default: _monospace_. *-w*,*--window-size-pixels*=_WIDTHxHEIGHT_ Set initial window width and height, in pixels. Default: _700x500_. *-W*,*--window-size-chars*=_WIDTHxHEIGHT_ Set initial window width and height, in characters. Default: _not set_. *-t*,*--term*=_TERM_ Value to set the environment variable *TERM* to (see *TERMINFO* and *ENVIRONMENT*). Default: _@default_terminfo@_. *-T*,*--title*=_TITLE_ Initial window title. Default: _foot_. *-a*,*--app-id*=_ID_ Value to set the *app-id* property on the Wayland window to. Default: _foot_ (normal mode), or _footclient_ (server mode). *-m*,*--maximized* Start in maximized mode. If both *--maximized* and *--fullscreen* are specified, the _last_ one takes precedence. *-F*,*--fullscreen* Start in fullscreen mode. If both *--maximized* and *--fullscreen* are specified, the _last_ one takes precedence. *-L*,*--login-shell* Start a login shell, by prepending a '-' to argv[0]. *--pty* Display an existing pty instead of creating one. This is useful for interacting with VM consoles. This option is not currently supported in combination with *-s*,*--server*. *-D*,*--working-directory*=_DIR_ Initial working directory for the client application. Default: _CWD of foot_. *-s*,*--server*[=_PATH_|_FD_] Run as a server. In this mode, a single foot instance hosts multiple terminals (windows). Use *footclient*(1) to launch new terminals. This saves some memory since for example fonts and glyph caches can be shared between the terminals. It also saves upstart time since the config has already been loaded and parsed, and most importantly, fonts have already been loaded (and their glyph caches are likely to already have been populated). Each terminal will have its own rendering threads, but all Wayland communication, as well as input/output to the shell, is multiplexed in the main thread. Thus, this mode might result in slightly worse performance when multiple terminals are under heavy load. Also be aware that should one terminal crash, it will take all the others with it. The default path is *$XDG\_RUNTIME\_DIR/foot-$WAYLAND\_DISPLAY.sock*. If *$XDG\_RUNTIME\_DIR* is not set, the default path is instead */tmp/foot.sock*. If *$XDG\_RUNTIME\_DIR* is set, but *$WAYLAND\_DISPLAY* is not, the default path is *$XDG\_RUNTIME\_DIR/foot.sock*. Note that if you change the default, you will also need to use the *--server-socket* option in *footclient*(1) and point it to your custom socket path. If the argument is a number, foot will interpret it as the file descriptor of a socket provided by a supervision daemon (such as systemd or s6), and use that socket as it's own. Two systemd units (foot-server.{service,socket}) are provided to use that feature with systemd. To use socket activation, only enable the socket unit. Note that starting *foot --server* as a systemd service will use the environment of the systemd user instance; thus, you'll need to import *$WAYLAND_DISPLAY* in it using *systemctl --user import-environment WAYLAND_DISPLAY*. *-H*,*--hold* Remain open after child process exits. *-p*,*--print-pid*=_FILE_|_FD_ Print PID to this file, or FD, when successfully started. The file (or FD) is closed immediately after writing the PID. When a _FILE_ as been specified, the file is unlinked at exit. This option can only be used in combination with *-s*,*--server*. *-d*,*--log-level*={*info*,*warning*,*error*,*none*} Log level, used both for log output on stderr as well as syslog. Default: _warning_. *-l*,*--log-colorize*=[{*never*,*always*,*auto*}] Enables or disables colorization of log output on stderr. Default: _auto_. *-S*,*--log-no-syslog* Disables syslog logging. Logging is only done on stderr. This option can only be used in combination with *-s*,*--server*. *-v*,*--version* Show the version number and quit. *-e* Ignored; for compatibility with *xterm -e*. This option was added in response to several program launchers passing *-e* to arbitrary terminals, under the assumption that they all implement the same semantics for it as *xterm*(1). Ignoring it allows foot to be invoked as e.g. *foot -e man foot* with the same results as with xterm, instead of producing an "invalid option" error. # KEYBOARD SHORTCUTS The following keyboard shortcuts are available by default. They can be changed in *foot.ini*(5). There are also more actions (disabled by default) available; see *foot.ini*(5). ## NORMAL MODE *shift*+*page up*/*page down* Scroll up/down in history *ctrl*+*shift*+*c*, *XF86Copy* Copy selected text to the _clipboard_ *ctrl*+*shift*+*v*, *XF86Paste* Paste from _clipboard_ *shift*+*insert* Paste from the _primary selection_ *ctrl*+*shift*+*r* Start a scrollback search *ctrl*+*+*, *ctrl*+*=* Increase font size *ctrl*+*-* Decrease font size *ctrl*+*0* Reset font size *ctrl*+*shift*+*n* Spawn a new terminal. If the shell has been configured to emit the _OSC 7_ escape sequence, the new terminal will start in the current working directory. *ctrl*+*shift*+*o* Activate URL mode, allowing you to "launch" URLs. *ctrl*+*shift*+*u* Activate Unicode input. *ctrl*+*shift*+*z* Jump to the previous, currently not visible, prompt. Requires shell integration. *ctrl*+*shift*+*x* Jump to the next prompt. Requires shell integration. ## SCROLLBACK SEARCH These keyboard shortcuts affect the search selection: *ctrl*+*r* Search _backward_ for the next match. If the search string is empty, the last searched-for string is used. *ctrl*+*s* Search _forward_ for the next match. If the search string is empty, the last searched-for string is used. *shift*+*right* Extend current selection to the right by one character. *shift*+*left* Extend current selection to the left by one character. *ctrl*+*w*, *ctrl*+*shift*+*right* Extend current selection (and thus the search criteria) to the end of the word, or the next word if currently at a word separating character. *ctrl*+*shift*+*w* Same as *ctrl*+*w*, except that the only word separating characters are whitespace characters. *ctrl*+*shift*+*left* Extend current selection to the left to the last word boundary. *ctrl*+*shift*+*w* Extend the current selection to the right to the last whitespace. *shift*+*down* Extend current selection down one line *shift*+*up* Extend current selection up one line. *ctrl*+*v*, *ctrl*+*shift*+*v*, *ctrl*+*y*, *XF86Paste* Paste from clipboard into the search buffer. *shift*+*insert* Paste from primary selection into the search buffer. *escape*, *ctrl*+*g*, *ctrl*+*c* Cancel the search *return* Finish the search and copy the current match to the primary selection. The terminal selection is kept, allowing you to press *ctrl*+*shift*+*c* to copy it to the clipboard. These shortcuts affect the search box in scrollback-search mode: *ctrl*+*b* Moves the cursor in the search box one **character** to the left. *ctrl*+*left*, *alt*+*b* Moves the cursor in the search box one **word** to the left. *ctrl*+*f* Moves the cursor in the search box one **character** to the right. *ctrl*+*right*, *alt*+*f* Moves the cursor in the search box one **word** to the right. *Home*, *ctrl*+*a* Moves the cursor in the search box to the beginning of the input. *End*, *ctrl*+*e* Moves the cursor in the search box to the end of the input. *alt*+*backspace*, *ctrl*+*backspace* Deletes the **word before** the cursor. *alt*+*delete*, *ctrl*+*delete* Deletes the **word after** the cursor. *ctrl*+*u* Deletes from the cursor to the start of the input *ctrl*+*k* Deletes from the cursor to the end of the input These shortcuts affect scrolling in scrollback-search mode: *shift*+*page-up* Scrolls up/back one page in history. *shift*+*page-down* Scroll down/forward one page in history. ## URL MODE *t* Toggle URL visibility in jump label. *escape*, *ctrl*+*g*, *ctrl*+*c*, *ctrl*+*d* Exit URL mode without launching a URL. ## MOUSE SHORTCUTS *left*, single-click Drag to select; when released, the selected text is copied to the _primary_ selection. This feature is normally *disabled* whenever the client has enabled _mouse tracking_, but can be forced by holding *shift*. Holding *ctrl* will create a block selection. *left*, double-click Selects the _word_ (separated by spaces, period, comma, parenthesis etc) under the pointer. Hold *ctrl* to select everything under the pointer up to, and until, the next space characters. *left*, triple-click Selects the everything between enclosing quotes, or the entire row if not inside a quote. *left*, quad-click Selects the entire row *middle* Paste from the _primary_ selection *right* Extend current selection. Clicking immediately extends the selection, while hold-and-drag allows you to interactively resize the selection. *ctrl*+*right* Extend the current selection, but force it to be character wise, rather than depending on the original selection mode. *wheel* Scroll up/down in history *ctrl*+*wheel* Increase/decrease font size ## TOUCHSCREEN *tap* Emulates mouse left button click. *drag* Scrolls up/down in history. Holding for a while before dragging (time delay can be configured) emulates mouse dragging with left button held. # FONT FORMAT The font is specified in FontConfig syntax. That is, a colon-separated list of font name and font options. _Examples_: - Dina:weight=bold:slant=italic - Courier New:size=12 # URLs Foot supports URL detection. But, unlike many other terminal emulators, where URLs are highlighted when they are hovered and opened by clicking on them, foot uses a keyboard driven approach. Pressing *ctrl*+*shift*+*o* enters _"Open URL mode"_, where all currently visible URLs are underlined, and is associated with a _"jump-label"_. The jump-label indicates the _key sequence_ (e.g. *"AF"*) to use to activate the URL. The key binding can, of course, be customized, like all other key bindings in foot. See *show-urls-launch* and *show-urls-copy* in *foot.ini*(5). *show-urls-launch* by default opens the URL with *xdg-open*. This can be changed with the *url-launch* option. *show-urls-copy* is an alternative to *show-urls-launch*, that changes what activating a URL _does_; instead of opening it, it copies it to the clipboard. It is unbound by default. Jump label colors, the URL underline color, and the letters used in the jump label key sequences can be configured. # ALT/META CHARACTERS By default, foot prefixes meta characters with *ESC*. This corresponds to XTerm's *metaSendsEscape* option set to *true*. This can be disabled programmatically with *\E[?1036l* (and enabled again with *\E[?1036h*). When disabled, foot will instead set the 8:th bit of meta character and then UTF-8 encode it. This corresponds to XTerm's *eightBitMeta* option set to *true*. This can also be disabled programmatically with *rmm* (Reset Meta Mode, *\E[?1034l*), and enabled again with *smm* (Set Meta Mode, *\E[?1034h*). # BACKSPACE Foot transmits DEL (*^?*) on backspace. This corresponds to XTerm's *backarrowKey* option set to *false*, and to DECBKM being _reset_. To instead transmit BS (*^H*), press *ctrl*+*backspace*. Note that foot does *not* implement DECBKM, and that the behavior described above *cannot* be changed. Finally, pressing *alt* will prefix the transmitted byte with ESC. # KEYPAD By default, *Num Lock* overrides the run-time configuration keypad mode; when active, the keypad is always considered to be in _numerical_ mode. This corresponds to XTerm's *numLock* option set to *true*. In this mode, the keypad keys always sends either numbers (Num Lock is active) or cursor movement keys (up, down, left, right, page up, page down etc). This can be disabled programmatically with *\E[?1035l* (and enabled again with *\E[?1035h*). When disabled, the keypad sends custom escape sequences instead of numbers, when in _application_ mode. # CONFIGURATION foot will search for a configuration file in the following locations, in this order: - *XDG_CONFIG_HOME/foot/foot.ini* (defaulting to *$HOME/.config/foot/foot.ini* if unset) - *XDG_CONFIG_DIRS/foot/foot.ini* (defaulting to */etc/xdg/foot/foot.ini* if unset) An example configuration file containing all options with their default value commented out will usually be installed to */etc/xdg/foot/foot.ini*. For more information, see *foot.ini*(5). # SHELL INTEGRATION ## Current working directory New foot terminal instances (bound to *ctrl*+*shift*+*n* by default) will open in the current working directory, if the shell in the "parent" terminal reports directory changes. This is done with the OSC-7 escape sequence. Most shells can be scripted to do this, if they do not support it natively. See the wiki (https://codeberg.org/dnkl/foot/wiki#user-content-spawning-new-terminal-instances-in-the-current-working-directory) for details. ## Jumping between prompts Foot can move the current viewport to focus prompts of already executed commands (bound to *ctrl*+*shift*+*z*/*x* by default). For this to work, the shell needs to emit an OSC-133;A (*\\E]133;A\\E\\\\*) sequence before each prompt. In zsh, one way to do this is to add a _precmd_ hook: *precmd() { print -Pn "\\e]133;A\\e\\\\" }* See the wiki (https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts) for details, and examples for other shells. ## Piping last command's output The key binding *pipe-command-output* can pipe the last command's output to an application of your choice (similar to the other *pipe-\** key bindings): *\[key-bindings\]++ pipe-command-output=[sh -c "f=$(mktemp); cat - > $f; footclient emacsclient -nw $f; rm $f"] Control+Shift+g* When pressing *ctrl*+*shift*+*g*, the last command's output is written to a temporary file, then an emacsclient is started in a new footclient instance. The temporary file is removed after the footclient instance has closed. For this to work, the shell must emit an OSC-133;C (*\\E]133;C\\E\\\\*) sequence before command output starts, and an OSC-133;D (*\\E]133;D\\E\\\\*) when the command output ends. In fish, one way to do this is to add _preexec_ and _postexec_ hooks: *function foot_cmd_start --on-event fish_preexec echo -en "\\e]133;C\\e\\\\" end* *function foot_cmd_end --on-event fish_postexec echo -en "\\e]133;D\\e\\\\" end* See the wiki (https://codeberg.org/dnkl/foot/wiki#user-content-piping-last-commands-output) for details, and examples for other shells # TERMINFO Client applications use the terminfo identifier specified by the environment variable *TERM* (set by foot) to determine terminal capabilities. Foot has two terminfo definitions: *foot* and *foot-direct*, with *foot* being the default. The difference between the two is in the number of colors they describe; *foot* describes 256 colors and *foot-direct* 16.7 million colors (24-bit truecolor). Note that using the *foot* terminfo does not limit the number of usable colors to 256; applications can still use 24-bit RGB colors. In fact, most applications work best with *foot* (including 24-bit colors). Using *\*-direct* terminfo entries has been known to crash some ncurses applications even. There are however applications that need a *\*-direct* terminfo entry for 24-bit support. Emacs is one such example. While using either *foot* or *foot-direct* is strongly recommended, it is possible to use e.g. *xterm-256color* as well. This can be useful when remoting to a system where foot's terminfo entries cannot easily be installed. Note that terminfo entries can be installed in the user's home directory. I.e. if you do not have root access, or if there is no distro package for foot's terminfo entries, you can install foot's terminfo entries manually, by copying *foot* and *foot-direct* to *~/.terminfo/f/*. # XTGETTCAP *XTGETTCAP* is an escape sequence initially introduced by XTerm, and also implemented (and extended, to some degree) by Kitty. It allows querying the terminal for terminfo classic, file-based, terminfo definition. For example, if all applications used this feature, you would no longer have to install foot's terminfo on remote hosts you SSH into. XTerm's implementation (as of XTerm-370) only supports querying key (as in keyboard keys) capabilities, and three custom capabilities: - TN - terminal name - Co - number of colors (alias for the colors capability) - RGB - number of bits per color channel (different semantics from the RGB capability in file-based terminfo definitions!). Kitty has extended this, and also supports querying all integer and string capabilities. Foot supports this, and extends it even further, to also include boolean capabilities. This means foot's entire terminfo can be queried via *XTGETTCAP*. Note that both Kitty and foot handles responses to multi-capability queries slightly differently, compared to XTerm. XTerm will send a single DCS reply, with ;-separated capability/value pairs. There are a couple of issues with this: - The success/fail flag in the beginning of the response is always 1 (success), unless the very first queried capability is invalid. - XTerm will not respond at all to an invalid capability, unless it's the first one in the XTGETTCAP query. - XTerm will end the response at the first invalid capability. In other words, if you send a large multi-capability query, you will only get responses up to, but not including, the first invalid capability. All subsequent capabilities will be dropped. Kitty and foot on the other hand, send one DCS response for each capability in the multi query. This allows us to send a proper success/fail flag for each queried capability. Responses for all queried capabilities are always sent. No queries are ever dropped. # EXIT STATUS Foot will exit with code 230 if there is a failure in foot itself. In all other cases, the exit code is that of the client application (i.e. the shell). # ENVIRONMENT ## Variables used by foot *SHELL* The default child process to run, when no _command_ argument is specified and the *shell* option in *foot.ini*(5) is not set. *HOME* Used to determine the location of the configuration file, see *foot.ini*(5) for details. *XDG\_CONFIG\_HOME* Used to determine the location of the configuration file, see *foot.ini*(5) for details. *XDG\_CONFIG\_DIRS* Used to determine the location of the configuration file, see *foot.ini*(5) for details. *XDG\_RUNTIME\_DIR* Used to construct the default _PATH_ for the *--server* option, when no explicit argument is given (see above). *WAYLAND\_DISPLAY* Used to construct the default _PATH_ for the *--server* option, when no explicit argument is given (see above). *XCURSOR\_THEME* The name of the *Xcursor*(3) theme to use for pointers (typically set by the Wayland compositor). *XCURSOR\_SIZE* The size to use for *Xcursor*(3) pointers (typically set by the Wayland compositor). ## Variables set in the child process *TERM* terminfo/termcap identifier. This is used by client applications to determine which capabilities a terminal supports. The value is set according to either the *--term* command-line option or the *term* config option in *foot.ini*(5). *COLORTERM* This variable is set to *truecolor*, to indicate to client applications that 24-bit RGB colors are supported. *PWD* Current working directory (at the time of launching foot) *SHELL* Set to the launched shell, if the shell is valid (it is listed in */etc/shells*). In addition to the variables listed above, custom environment variables may be defined in *foot.ini*(5). ## Variables *unset* in the child process *TERM_PROGRAM* *TERM_PROGRAM_VERSION* These environment variables are set by certain other terminal emulators. We unset them, to prevent applications from misdetecting foot. In addition to the variables listed above, custom environment variables to unset may be defined in *foot.ini*(5). # BUGS Please report bugs to https://codeberg.org/dnkl/foot/issues Before you open a new issue, please search existing bug reports, both open and closed ones. Chances are someone else has already reported the same issue. The report should contain the following: - Foot version (*foot --version*). - Log output from foot (run *foot -d info* from another terminal). - Which Wayland compositor (and version) you are running. - If reporting a crash, please try to provide a *bt full* backtrace with symbols. - Steps to reproduce. The more details the better. # IRC \#foot on irc.libera.chat # SEE ALSO *foot.ini*(5), *footclient*(1) foot-1.21.0/doc/foot.ini.5.scd000066400000000000000000002057161476600145200157120ustar00rootroot00000000000000foot.ini(5) # NAME foot.ini - configuration file for *foot*(1) # DESCRIPTION *foot* uses the standard _unix configuration format_, with section based key/value pairs. The default section is usually unnamed, i.e. not prefixed with a _[section]_. However it can also be explicitly named _[main]_, say if it needs to be reopened after any of the other sections. foot will search for a configuration file in the following locations, in this order: - *XDG_CONFIG_HOME/foot/foot.ini* (defaulting to *$HOME/.config/foot/foot.ini* if unset) - *XDG_CONFIG_DIRS/foot/foot.ini* (defaulting to */etc/xdg/foot/foot.ini* if unset) An example configuration file containing all options with their default value commented out will usually be installed to */etc/xdg/foot/foot.ini*. Options are set using KEY=VALUE pairs: *\[colors\]*++ *background=000000*++ *foreground=ffffff* Empty values (*KEY=*) are not supported. String options do allow the empty string to be set, but it must be quoted: *KEY=""*) # SECTION: main *shell* Executable to launch. Typically a shell. Default: _$SHELL_ if set, otherwise the user's default shell (as specified in _/etc/passwd_). You can also pass arguments. For example */bin/bash --norc*. *login-shell* Boolean. If enabled, the shell will be launched as a login shell, by prepending a '-' to argv[0]. Default: _no_. *term* Value to set the environment variable *TERM* to. Default: _@default_terminfo@_ *font*, *font-bold*, *font-italic*, *font-bold-italic* Comma separated list of fonts to use, in fontconfig format. That is, a font name followed by a list of colon-separated options. Most noteworthy is *:size=n* (or *:pixelsize=n*), which is used to set the font size. Note that the font size is also affected by the *dpi-aware* option. Examples: - Dina:weight=bold:slant=italic - Courier New:size=12 - Fantasque Sans Mono:fontfeatures=ss01 - Iosevka:fontfeatures=cv01=1:fontfeatures=cv06=1 - Meslo LG S:size=12, Noto Color Emoji:size=12 - Courier New:pixelsize=8 Be aware that, depending on your setup, there may be global FontConfig options that overrides options set here. If an option appears to have no effect, ensure there is no global configuration file that sets the same option with *assign* or *assign_replace*; use one of the many *append* or possibly *prepend* modes. For each option, the first font is the primary font. The remaining fonts are fallback fonts that will be used whenever a glyph cannot be found in the primary font. The fallback fonts are searched in the order they appear. If a glyph cannot be found in any of the fallback fonts, the dynamic fallback list from fontconfig (for the primary font) is searched. *font-bold*, *font-italic* and *font-bold-italic* allow custom fonts to be used for bold/italic/bold+italic fonts. If left unconfigured, the bold/italic variants of the regular font(s) specified in *font* are used. *Note*: you _may_ have to tweak the size(s) of the custom bold/italic fonts to match the regular font. To disable bold and/or italic fonts, set e.g. *font-bold* to _exactly_ the same value as *font*. **size** is in _points_ (as defined by the FontConfig format). To set a _pixel_ size, use **pixelsize** instead. Note that pixel sizes are unaffected by DPI aware rendering (see *dpi-aware*), but are affected by desktop scaling. Default: _monospace:size=8_ (*font*), _not set_ (*font-bold*, *font-italic*, *font-bold-italic*). *font-size-adjustment* Amount, in _points_, _pixels_ or _percent_, to increment/decrement the font size when zooming in our out. Examples: ``` font-size-adjustment=0.5 # Adjust by 0.5 points font-size-adjustment=10px # Adjust by 10 pixels font-size-adjustment=7.5% # Adjust by 7.5 percent ``` Default: _0.5_ *include* Absolute path to configuration file to import. The import file has its own section scope. I.e. the including configuration is still in the default section after the include, regardless of which section the included file ends in. - The path must be an absolute path, or start with *~/*. - Multiple include directives are allowed, but only one path per directive. - Nested imports are allowed. Default: _not set_. *line-height* An absolute value, in _points_, that override line height from the font metrics. You can specify a height in _pixels_ by using the *px* suffix: e.g. *line-height=12px*. *Warning*: when changing the font size at runtime (i.e. zooming in our out), foot will change the line height by the same percentage. However, due to rounding, it is possible the line height will be "too small" for some font sizes, causing e.g. underscores to "disappear". See also: *vertical-letter-offset*. Default: _not set_. *letter-spacing* Spacing between letters, in _points_. A positive value will increase the cell size, and a negative value shrinks it. You can specify a letter spacing in _pixels_ by using the *px* suffix: e.g. *letter-spacing=2px*. See also: *horizontal-letter-offset*. Default: _0_. *horizontal-letter-offset*, *vertical-letter-offset* Configure the horizontal and vertical offsets used when positioning glyphs within cells, in _points_, relative to the top left corner. To specify an offset in _pixels_, append *px*: e.g. *horizontal-letter-offset=2px*. Default: _0_. *underline-offset* Use a custom offset for underlines. The offset is, by default, in _points_ and relative the font's baseline. A positive value positions the underline under the baseline, while a negative value positions it above the baseline. To specify an offset in _pixels_, append *px*: *underline-offset=2px*. If left unset (the default), the offset specified in the font is used, or estimated by foot if the font lacks underline positioning information. Default: _unset_. *underline-thickness* Use a custom thickness (height) for underlines. The thickness is, by default, in _points_. To specify a thickness in _pixels_, append *px*: *underline-thickness=1px*. If left unset (the default), the thickness specified in the font is used. Default: _unset_ *strikeout-thickness* Use a custom thickness (height) for strikeouts. The thickness is, by default, in _points_. To specify a thickness in _pixels_, append *px*: *strikeout-thickness=1px*. If left unset (the default), the thickness specified in the font is used. Default: _unset_ *gamma-correct-blending* Boolean. When enabled, foot will do gamma-correct blending in linear color space. This is how font glyphs are supposed to be rendered, but since nearly no applications or toolkits are doing it on Linux, the result may not look like you are used to. Compared to the default (disabled), bright glyphs on a dark background will appear thicker, and dark glyphs on a light background will appear thinner. 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. 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. You may also want to enable 10-bit image buffers when gamma-correct blending is enabled. Though probably only if you do not use a transparent background (with 10-bit buffers, you only get 2 bits alpha). See *tweak.surface-bit-depth*. Default: enabled when compositor support is available *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: - No antialiasing effects where e.g. line endpoints appear dimmed down, or blurred. - Line- and box characters are guaranteed to span the entire cell, resulting in a gap-less appearance. - No alignment issues, i.e. lines are centered when they should be. - Many fonts lack some, or all, of the line- and box drawing characters, causing fallback fonts to be used, which results in out-of-place looking glyphs (for example, badly sized). When enabled, box/line drawing characters are rendered using font glyphs. This may result in a more uniform look, in some use cases. When disabled, foot will render the following Unicode codepoints by itself: - U+02500 - U+0259F - U+02800 - U+028FF - U+1Fb00 - U+1FB9B Default: _no_. *dpi-aware* Boolean. When set to *yes*, fonts are sized using the monitor's DPI, making a font of a given size have the same physical size, regardless of monitor. In other words, if you drag a foot window between different monitors, the font size remains the same. In this mode, the monitor's scaling factor is ignored; doubling the scaling factor will *not* double the font size. When set to *no*, the monitor's DPI is ignored. The font is instead sized using the monitor's scaling factor; doubling the scaling factor *does* double the font size. Note that this option typically does not work with bitmap fonts, which only contains a pre-defined set of sizes, and cannot be dynamically scaled. Whichever size (of the available ones) that best matches the DPI or scaling factor, will be used. Also note that if the font size has been specified in pixels (*:pixelsize=*_N_, instead of *:size=*_N_), DPI scaling (*dpi-aware=yes*) will have no effect (the specified pixel size will be used as is). But, if the monitor's scaling factor is used to size the font (*dpi-aware=no*), the font's pixel size will be multiplied with the scaling factor. Default: _no_ *pad* Padding between border and glyphs, in pixels (subject to output scaling), in the form _XxY_. 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. To instead center the grid content, append *center* (e.g. *pad=5x5 center*). Default: _0x0_. *resize-delay-ms* Time, in milliseconds, of "idle time" before foot performs text reflow, and sends the new window dimensions to the client application while doing an interactive resize of a foot window. Idle time in this context is a period of time where the window size is not changing. In other words, while you are fiddling with the window size, foot does not send the updated dimensions to the client. It also does a fast "truncating" resize of the grid, instead of actually reflowing the contents. Only when you pause the fiddling for *resize-delay-ms* milliseconds is the client updated, and the contents properly reflowed. Emphasis is on _while_ here; as soon as the interactive resize ends (i.e. when you let go of the window border), the final dimensions is 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 maxmized, 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-window-size-pixels* Initial window width and height in _pixels_ (subject to output scaling), in the form _WIDTHxHEIGHT_. The height _includes_ the titlebar when using CSDs. Mutually exclusive to *initial-window-size-chars*. Note that this option may not work as expected if fractional scaling is being used, due to the fact that many compositors do not report the correct scaling factor until after a window has been mapped. Default: _700x500_. *initial-window-size-chars* Initial window width and height in _characters_, in the form _WIDTHxHEIGHT_. Mutually exclusive to *initial-window-size-pixels*.' Note that if you have a multi-monitor setup, with different scaling factors, there is a possibility the window size will not be set correctly. If that is the case, use *initial-window-size-pixels* instead. And, just like *initial-window-size-pixels*, this option may not work as expected if fractional scaling is being used (see *initial-window-size-pixels* for details). Default: _not set_. *initial-window-mode* Initial window mode for each newly spawned window: *windowed*, *maximized* or *fullscreen*. Default: _windowed_. *title* Initial window title. Default: _foot_. *locked-title* Boolean. If enabled, applications are not allowed to change the title at run-time. Default: _no_. *app-id* Value to set the *app-id* property on the Wayland window to. The compositor can use this value to e.g. group multiple windows, or apply window management rules. Default: _foot_ (normal mode), or _footclient_ (server mode). *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. If set to *palette-based*, rather than a simple *yes|true*, colors matching one of the 8 regular palette colors will be brightened using the corresponding bright palette color. Other colors will not be brightened. Default: _no_. *word-delimiters* String of characters that act as word delimiters when selecting text. Note that whitespace characters are _always_ word delimiters, regardless of this setting. Default: _,│`|:"'()[]{}<>_ *selection-target* Clipboard target to automatically copy selected text to. One of *none*, *primary*, *clipboard* or *both*. Default: _primary_. *workers* Number of threads to use for rendering. Set to 0 to disable multithreading. Default: the number of available logical CPUs (including SMT). Note that this is not always the best value. In some cases, the number of physical _cores_ is better. *utmp-helper* Path to utmp logging helper binary. When starting foot, an utmp record is created by launching the helper binary with the following arguments: ``` @utmp_add_args@ ``` When foot is closed, the utmp record is removed by launching the helper binary with the following arguments: ``` @utmp_del_args@ ``` Set to *none* to disable utmp records. Default: _@utmp_helper_path@_. # SECTION: environment This section is used to define environment variables that will be set in the client application, in addition to the variables inherited from the terminal process itself. The format is simply: *name*=_value_ Note: do not set *TERM* here; use the *term* option in the main (default) section instead. # SECTION: security *osc52* Whether OSC-52 (clipboard access) is enabled or disabled. One of *disabled*, *copy-enabled*, *paste-enabled* or *enabled*. OSC-52 gives terminal application access to the host clipboard (i.e. the Wayland clipboard). This is normally not a security issue, since all applications can access the clipboard directly over the Wayland socket. However, when SSH:ing into a remote system, or accessing a container etc, the terminal applications may be untrusted, and you might consider disabling the host clipboard access. - *disabled*: disables all clipboard access - *copy-enabled*: applications can write to the clipboard, but not read from it. - *paste-enabled*: applications can read from the clipboard, but not write to it. - *enabled*: all applications have full access to the host clipboard. This is the default. Default: _enabled_ # SECTION: bell *system* Boolean, when set to _yes_, ring the system bell. The bell is rung independent of whether the foot window has keyboard focus or not. Exact behavior is compositor dependent. Default: _yes_ *urgent* Boolean, when set to _yes_, foot will signal urgency to the compositor through the XDG activation protocol whenever *BEL* is received, and the window does NOT have keyboard focus. If the compositor does not implement this protocol, the margins will be painted in red instead. Applications can enable/disable this feature programmatically with the *CSI ? 1042 h* and *CSI ? 1042 l* escape sequences. Default: _no_ *notify* Boolean, when set to _yes_, foot will emit a desktop notification using the command specified in the *notify* option whenever *BEL* is received. By default, bell notifications are shown only when the window does *not* have keyboard focus. See _desktop-notifications.inhibit-when-focused_. Default: _no_ *visual* Boolean, when set to _yes_, foot will flash the terminal window. Default: _no_ *command* When set, foot will execute this command when *BEL* is received. Default: none *command-focused* Boolean, whether to run the command on *BEL* even while focused. Default: _no_ # SECTION: desktop-notifications *command* Command to execute to display a notification. Template arguments _${title}_ and _${body}_ will be replaced with the notification's actual _title_ and _body_ (message content). _${app-id}_ is replaced with the value of the command line option _--app-id_, and defaults to *foot* (normal mode), or *footclient* (server mode). _${window-title}_ is replaced with the current window title. _${icon}_ is replaced by the icon specified in the notification request, or the empty string if no icon was specified. Can be used with e.g. notify-send's *--icon* option, or preferably, by setting the *image-path* hint (with e.g. notify-send's *--hint* option). _${category}_ is replaced by the notification's catogory. Can be used together with e.g. notify-send's *--category* option. _${urgency}_ is replaced with the notifications urgency; *low*, *normal* or *critical*. Can be used together with e.g. notify-send's *--urgency* option. _${expire-time}_ is replaced with the notification specified notification timeout. Can be used together with e.g. notify-send's *--expire-time* option. _${replace-id}_ is replaced by the notification daemon assigned ID that the notification replaces/updates. For this to work, foot needs to know the externally assigned IDs of previously emitted notifications, see the 'stdout' section below. Can be used together with e.g. notify-send's *--replace-id* option. _${muted}_ is replaced by either *true* or *false*, depending on whether the notification has requested all notification sounds be muted. It is intended to set the *suppress-sound* hint (with e.g. notify-send's *--hint* option). _${sound-name}_ is replaced by sound-name requested by the notification. This should be a name from the freedesktop sound naming specification, but this is not something that foot enforces. It is intended to set the *sound-name* hint (with e.g. notify-send's *--hint* option). _${action-argument}_ will be expanded to the *command-action-argument* option, for each notification action. There will always be at least one action, the "default" action. Foot uses this to enable window focusing, and reporting notification activation to applications that requested such events. Applications can also define their own custom notification actions. See the *command-action-argument* option for details. Ways to trigger notifications Applications can trigger notifications in the following ways: - OSC 777: *\\e]777;notify;;<body>\\e\\\\* - OSC 99: *\\e]99;;<title>\\e\\\\* (this is just a bare bones example; this protocol has lots of features, see https://sw.kovidgoyal.net/kitty/desktop-notifications) By default, notifications are *inhibited* if the foot window has keyboard focus. See _desktop-notifications.inhibit-when-focused_. Window activation (focusing) Foot can focus the window when the notification is 'activated'. It can also send an event back to the client application, notifying it that the notification has been 'activated', This typically happens when the default action is invoked, and/or when the notification is clicked, but exact behavior depends on the notification daemon in use, and how it has been configured. For this to work, foot needs to know when the notification was activated (as opposed to just dismissed), and it needs an XDG activation token. There are two parts to handle this. First, the notification must define an action. For this purpose, foot will add a "default" action to the notification (see the *command-action-argument* option). Second, foot needs to know when the notification is activated, and it needs to get hold of the XDG activation token. Both are expected to be printed on stdout. Foot expects the action name (not label) to be printed on a single line. No prefix, no postfix. Foot expects the activation token to be printed on a single line, prefixed with *xdgtoken=*. Example: default++ xdgtoken=18179adf579a7a904ce73754964b1ec3 The expected format of stdout may change at any time. Please read the changelog when upgrading foot. *Note*: notify-send does not, out of the box, support reporting the XDG activation token in any way. This means window activation will not work by default. Stdout Foot recognizes the following things from the notification helper's stdout: - _id_: integer in base 10, daemon assigned notification ID - *id=*_id_: same as plain _nnn_. - *default*: the 'default' action was triggered - *action=*_default_: same as _default_ - *action=*_n_: application custom action _n_ triggered - _n_: integer in base 10, appearing after the ID; application custom action _n_ triggered - *xdgtoken=*_xyz_: XDG activation token. Example #1: 17++ action=default++ xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 Foot recognizes this as: - notification has the daemon assigned ID 17 - the user triggered the default action - the notification send 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 *command* to the full list of actions. If *command-action-argument* is set to the empty string, no actions will be passed to *command*. That is, _${action-argument}_ will be replaced with the empty string. Example: *command-action-argument=--action ${action-name}=${action-label}*++ *command=notify-send ${action-argument} ...* Assume the application defined two custom actions: *OK* and *Cancel*. Given the above, foot will execute: notify-send++ --action default='Click to activate'++ --action 1=OK++ --action 2=Cancel++ ... Default: _--action ${action-name}=${action-label}_ *close* Command to execute to close an existing notification. _${id}_ is expanded to the ID of the notification that should be closed. For example: fyi --close ${id} Closing a notification is only supported by the Kitty Desktop Notification protocol, OSC-99. If set to the empty string (the default), foot will instead try to close the notification by sending SIGINT to the notification helper process. For example, *notify-send --wait* (libnotify >= 0.8.0) responds to SIGINT by closing the notification. Default: _not set_ *inhibit-when-focused* Boolean. If enabled, foot will not display notifications if the terminal window has keyboard focus. Default: _yes_ # SECTION: scrollback *lines* Number of scrollback lines. The maximum number of allocated lines will be this value plus the number of visible lines, rounded up to the nearest power of 2. Default: _1000_. *multiplier* Amount to multiply mouse scrolling with. It is a decimal number, i.e. fractions are allowed. Default: _3.0_. *indicator-position* Configures the style of the scrollback position indicator. One of *none*, *fixed* or *relative*. *none* disables the indicator completely. *fixed* always renders the indicator near the top of the window, and *relative* renders the indicator at the position corresponding to the current scrollback position. Default: _relative_. *indicator-format* Which format to use when displaying the scrollback position indicator. Either _percentage_, _line_, or a custom fixed string. This option is ignored if *indicator-position=none*. Default: _empty string_. # SECTION: url Note that you can also add custom regular expressions, see the 'regex' section. *launch* Command to execute when opening URLs. _${url}_ will be replaced with the actual URL. Default: _xdg-open ${url}_. *osc8-underline* When to underline OSC-8 URLs. Possible values are *url-mode* and *always*. When set to *url-mode*, OSC-8 URLs are only highlighted in URL mode, just like auto-detected URLs. When set to *always*, OSC-8 URLs are always highlighted, regardless of their other attributes (bold, italic etc). Note that this does _not_ make them clickable. Default: _url-mode_ *label-letters* String of characters to use when generating key sequences for URL jump labels. If you change this option to include the letter *t*, you should also change the default *[url-bindings].toggle-url-visible* key binding to avoid a clash. Default: _sadfjklewcmpgh_. *regex* Regular expression to use when auto-detecting URLs. The format is "POSIX-Extended Regular Expressions". Note that the first marked subexpression is used as the URL. In other words, if you want the whole regex match to be used as an URL, surround all of it with parenthesis: *(regex-pattern)*. Default: _(([a-z][[:alnum:]-]+:(/{1,3}|[a-z0-9%])|www[:digit:]{0,3}[.])([^[:space:](){}<>]+|\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))\*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))\*\])+(\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))\*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))\*\]|[^]\[[:space:]`!(){};:'".,<>?«»“”‘’]))_ # SECTION: regex Similar to the 'url' mode, but with custom defined regular expressions (and launchers). To use a custom defined regular expression, you also need to add a key binding for it. This is done in the *key-binding* section, see below for details. For example, a regex to detect hash digests (e.g. git commit hashes) could look like: ``` [regex:hashes] regex=([a-fA-F0-9]{7,128}) launch=path-to-script-or-application ${match} [key-bindings] regex-launch=[hashes] Control+Shift+q regex-copy=[hashes] Control+Mod1+Shift+q ``` *launch* Command to execute when "launching" a regex match. _${match}_ will be replaced with the actual URL. Default: _not set_. *regex* Regular expression to use when matching text. The format is "POSIX-Extended Regular Expressions". Note that the first marked subexpression is used as the match. In other words, if you want the whole regex match to be used, surround all of it with parenthesis: *(regex-pattern)*. Default: _not set_. # SECTION: cursor This section controls the cursor style and color. Note that applications can change these at runtime. *style* Configures the default cursor style, and is one of: *block*, *beam*, *underline* or *hollow*. Note that this can be overridden by applications. Default: _block_. *unfocused-style* Configures how the cursor is rendered when the terminal window is unfocused. Possible values are: - unchanged: render cursor in exactly the same way as when the window has focus. - hollow: render a block cursor, but hollowed out. - none: do not display any cursor at all. *blink* Boolean. Enables blinking cursor. Note that this can be overridden by applications. Related option: *blink-rate*. Default: _no_. *blink-rate* The rate at which the cursor blink, when cursor blinking has been enabled. Expressed in milliseconds between each blink. Default: _500_. *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. *beam-thickness* Thickness (width) of the beam styled cursor. The value is in points, and its exact value thus depends on the monitor's DPI. To instead specify a thickness in pixels, use the *px* suffix: e.g. *beam-thickness=2px*. Default: _1.5_ *underline-thickness* Thickness (height) of the underline styled cursor. The value is in points, and its exact value thus depends on the monitor's DPI. To instead specify a thickness in pixels, use the *px* suffix: e.g. *underline-thickness=2px*. Note that if left unset, the cursor's thickness will scale with the font size, while if set, the size is fixed. Default: _font underline thickness_. # SECTION: mouse *hide-when-typing* Boolean. When enabled, the mouse cursor is hidden while typing. Default: _no_. *alternate-scroll-mode* Boolean. This option controls the initial value for the _alternate scroll mode_. When this mode is enabled, mouse scroll events are translated to _up_/_down_ key events when displaying the alternate screen. This lets you scroll with the mouse in e.g. pagers (like _less_) without enabling native mouse support in them. Alternate scrolling is *not* used if the application enables native mouse support. This option can be modified by applications at run-time using the escape sequences *CSI ? 1007 h* (enable) and *CSI ? 1007 l* (disable). Default: _yes_. # SECTION: touch *long-press-delay* Number of milliseconds to distinguish between a short press and a long press on the touchscreen. Default: _400_. # SECTION: colors This section 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. *foreground* Default foreground color. This is the color used when no ANSI color is being used. Default: _839496_. *background* Default background color. This is the color used when no ANSI color is being used. Default: _002b36_. *regular0*, *regular1* *..* *regular7* The eight basic ANSI colors (Black, Red, Green, Yellow, Blue, Magenta, Cyan, White). Default: _242424_, _f62b5a_, _47b413_, _e3c401_, _24acd4_, _f2affd_, _13c299_, _e6e6e6_ (starlight theme, V4). *bright0*, *bright1* *..* *bright7* The eight bright ANSI colors (Black, Red, Green, Yellow, Blue, Magenta, Cyan, White). Default: _616161_, _ff4d51_, _35d450_, _e9e836_, _5dc5f8_, _feabf2_, _24dfc4_, _ffffff_ (starlight theme, V4). *dim0*, *dim1* *..* *dim7* Custom colors to use with dimmed colors. Dimmed colors do not have an entry in the color palette. Applications emit them by combining a color value, and a "dim" attribute. By default, foot implements this by reducing the luminance of the current color. This is a generic approach that applies to both colors from the 256-color palette, as well as 24-bit RGB colors. You can change this behavior by setting the *dimN* options. When set, foot will match the current color against the color palette, and if it matches one of the *regularN* colors, the corresponding *dimN* color will be used. If instead the current color matches one of the *brightN* colors, the corresponding *regularN* color will be used. If the current color does not match any known color, it is dimmed by reducing the luminance (i.e. the same behavior as if the *dimN* options are unconfigured). 24-bit RGB colors will typically fall into this category. Note that applications can change the *regularN* and *brightN* colors at runtime. However, they have no way of changing the *dimN* colors. If an application has changed the *regularN* colors, foot will still use the corresponding *dimN* color, as configured in foot.ini. Default: _not set_. *0* *..* *255* Arbitrary colors in the 256-color palette. Default: for *0* *..* *15*, see regular and bright defaults above; see https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit for an explanation of the remainder. *sixel0* *..* *sixel15* The default sixel color palette. Default: _000000_, _3333cc_, _cc2121_, _33cc33_, _cc33cc_, _33cccc_, _cccc33_, _878787_, _424242_, _545499_, _994242_, _549954_, _995499_, _549999_, _999954_, _cccccc_. *alpha* Background translucency. A value in the range 0.0-1.0, where 0.0 means completely transparent, and 1.0 is opaque. Default: _1.0_. *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_. *jump-labels* Two color values specifying the foreground (text) and background colors to use when rendering jump labels in URL mode. Default: _regular0 regular3_. *scrollback-indicator* Two color values specifying the foreground (text) and background (indicator itself) colors for the scrollback indicator. Default: _regular0 bright4_. *search-box-no-match* Two color values specifying the foreground (text) and background colors for the scrollback search box, when there are no matches. Default: _regular0 regular1_. *search-box-match* Two color values specifying the foreground (text) and background colors for the scrollback search box, when the search box is either empty, or there are matches. Default: _regular0 regular3_. *urls* Color to use for the underline used to highlight URLs in URL mode. Default: _regular3_. *flash* Color to use for the terminal window flash. Default: _7f7f00_. *flash-alpha* Flash translucency. A value in the range 0.0-1.0, where 0.0 means completely transparent, and 1.0 is opaque. Default: _0.5_. # SECTION: csd This section controls the look of the _CSDs_ (Client Side Decorations). Note that the default is to *not* use CSDs, but instead to use _SSDs_ (Server Side Decorations) when the compositor supports it. Note that unlike the colors defined in the _colors_ section, the color values here are in AARRGGBB (i.e. plain old 8-digit hex values) format. I.e. they contain an alpha component - 00 means completely transparent, and ff fully opaque. Examples: - ffffffff: white, fully opaque - ff000000: black, fully opaque - 7fffffff: white, semi-transparent - ff00ff00: green, fully opaque *preferred* Which type of window decorations to prefer: *client* (CSD), *server* (SSD) or *none*. Note that this is only a hint to the compositor. Depending on compositor support, and how it has been configured, it may instruct foot to use CSDs even though this option has been set to *server*, or render SSDs despite *client* or *none* being set. Default: _server_. *size* Height, in pixels (subject to output scaling), of the titlebar. Setting it to 0 will hide the titlebar, while still showing the border (if *border-width* is set to a non-zero value). Default: _26_. *color* Titlebar color. Default: use the default _foreground_ color. *font* Font to use for the title bar. This is a list of fonts, similar to the main *font* option. Note that the font will be sized using the title bar size. That is, all *:size* and *:pixelsize* attributes will be ignored. Default: _primary font_. *hide-when-maximized* Boolean. When enabled, the CSD titlebar is hidden when the window is maximized. The completely disable the titlebar, set *size* to 0 instead. Default: _no_. *double-click-to-maximize* Boolean. When enabled, double-clicking the CSD titlebar will (un)maximize the window. Default: _yes_. *border-width* Width of the border, in pixels (subject to output scaling). Note that the border encompasses the entire window, including the title bar. Default: _0_. *border-color* Color of border. By default, the title bar color is used. If the title bar color has not been set, the default foreground color (from the color scheme) is used. Default: _titlebar color_. *button-width* Width, in pixels (subject to output scaling), of the minimize/maximize/close buttons. Default: _26_. *button-color* Foreground color on the minimize/maximize/close buttons. Default: use the default _background_ color. *button-minimize-color* Minimize button's background color. Default: use the default _regular4_ color (blue). *button-maximize-color* Maximize button's background color. Default: use the default _regular2_ color (green). *button-close-color* Close button's background color. Default: use the default _regular1_ color (red). # SECTION: key-bindings This section lets you override the default key bindings. The general format is _action=combo1...comboN_. That is, each action may have one or more key combinations, space separated. Each combination is in the form _mod1+mod2+key_. The names of the modifiers and the key *must* be valid XKB key names. Note that if *Shift* is one of the modifiers, the _key_ *must not* be in upper case. For example, *Control+Shift+V* will never trigger, but *Control+Shift+v* will. Note that *Alt* is usually called *Mod1*. *xkbcli interactive-wayland* can be useful for finding keysym names. When matching key presses to key bindings, foot uses a couple of different approaches. As an example, let's say you press ctrl+shift+c (assume plain us ASCII layout). XKB will tell foot *Control+C* was pressed. Note the lack of the shift modifier, and the upper case 'C'. Internally, this is called the "translated" form, and is what foot tries to match first. If no "translated" key bindings can be found, foot proceeds to checking the "untranslated" variant. Using the same example as above, this will match *Control+Shift+c* (shift modifier present, lower case 'c'). This means you can use either form in your foot configuration, and that *Control+C* (and similar) has higher priority than *Control+Shift+c*. Also note that while foot normally detects when the same combination is assigned to multiple actions, it will not detect *Control+C* vs. *Control+Shift+c* collisions. Call it a known bug... Finally, foot tries to match the raw key code. Here, the primary layout is queried for all key codes that generate a particular XKB symbol, and the pressed key's code is matched against this. For example, if you use the layouts *"us,de(neo)"*, the 'r' key generates the symbol 'c' in the neo layout. I.e. to get a 'c', you press 'r'. The match logic described above will only match 'c' key bindings (e.g. *Control+Shift+c*). The raw mode however, will match 'r' key bindings (e.g. *Control+Shift+r*). This is useful for non-latin layouts, where you would otherwise have to customize all key bindings. A key combination can only be mapped to *one* action. Let's say you want to bind *Control+Shift+R* to *fullscreen*. Since this is the default shortcut for *search-start*, you first need to unmap the default binding. This can be done by setting _action=none_; e.g. *search-start=none*. *noop* All key combinations listed here will not be sent to the application. Default: _none_. *scrollback-up-page* Scrolls up/back one page in history. Default: _Shift+Page\_Up Shift+KP\_Page\_Up_. *scrollback-up-half-page* Scrolls up/back half of a page in history. Default: _none_. *scrollback-up-line* Scrolls up/back a single line in history. Default: _none_. *scrollback-down-page* Scroll down/forward one page in history. Default: _Shift+Page\_Down Shift+KP\_Page\_Down_. *scrollback-down-half-page* Scroll down/forward half of a page in history. Default: _none_. *scrollback-down-line* Scroll down/forward a single line in history. Default: _none_. *scrollback-home* Scroll to the beginning of the scrollback. Default: _none_. *scrollback-end* Scroll to the end (bottom) of the scrollback. Default: _none_. *clipboard-copy* Copies the current selection into the _clipboard_. Default: _Control+Shift+c_ _XF86Copy_. *clipboard-paste* Pastes from the _clipboard_. Default: _Control+Shift+v_ _XF86Paste_. *primary-paste* Pastes from the _primary selection_. Default: _Shift+Insert_ (also defined in *mouse-bindings*). *search-start* Starts a scrollback/history search. Default: _Control+Shift+r_. *font-increase* Increases the font size by 0.5pt. Default: _Control+plus Control+equal Control+KP\_Add_ (also defined in *mouse-bindings*). *font-decrease* Decreases the font size by 0.5pt. Default: _Control+minus Control+KP\_Subtract_ (also defined in *mouse-bindings*). *font-reset* Resets the font size to the default. Default: _Control+0 Control+KP\_0_. *spawn-terminal* Spawns a new terminal. If the shell has been configured to emit the OSC 7 escape sequence, the new terminal will start in the current working directory. Default: _Control+Shift+n_. *minimize* Minimizes the window. Default: _none_. *maximize* Toggle the maximized state. Default: _none_. *fullscreen* Toggles the fullscreen state. Default: _none_. *pipe-visible*, *pipe-scrollback*, *pipe-selected*, *pipe-command-output* Pipes the currently visible text, the entire scrollback, the currently selected text, or the last command's output to an external tool. The syntax for this option is a bit special; the first part of the value is the command to execute enclosed in "[]", followed by the binding(s). You can configure multiple pipes as long as the command strings are different and the key bindings are unique. Note that the command is *not* automatically run inside a shell; use *sh -c "command line"* if you need that. Example #1: # Extract currently visible URLs, let user choose one (via fuzzel), then launch firefox with the selected URL++ *pipe-visible=[sh -c "xurls | uniq | tac | fuzzel | xargs -r firefox"] Control+Print* Example #2: # Open scrollback contents in Emacs running in a new foot instance++ *pipe-scrollback=[sh -c "f=$(mktemp) && cat - > $f && foot emacsclient -t $f; rm $f"] Control+Shift+Print* Default: _none_ *show-urls-launch* Enter URL mode, where all currently visible URLs are tagged with a jump label with a key sequence that will open the URL (and exit URL mode). Default: _Control+Shift+o_. *show-urls-persistent* Similar to *show-urls-launch*, but does not automatically exit URL mode after activating an URL. Default: _none_. *show-urls-copy* Enter URL mode, where all currently visible URLs are tagged with a jump label with a key sequence that will place the URL in the clipboard. 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-copy*, but the match is placed in the clipboard, instead of "launched", upon activation. 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 *foot*(1)). Default: _Control+Shift+x_. *unicode-input* Input a Unicode character by typing its codepoint in hexadecimal, followed by *Enter* or *Space*. For example, to input the character _ö_ (LATIN SMALL LETTER O WITH DIAERESIS, Unicode codepoint 0xf6), you would first activate this key binding, then type: *f*, *6*, *Enter*. Another example: to input 😍 (SMILING FACE WITH HEART-SHAPED EYES, Unicode codepoint 0x1f60d), activate this key binding, then type: *1*, *f*, *6*, *0*, *d*, *Enter*. Recognized key bindings in Unicode input mode: - Enter, Space: commit the Unicode character, then exit this mode. - Escape, q, Ctrl+c, Ctrl+d, Ctrl+g: abort input, then exit this mode. - 0-9, a-f: append next digit to the Unicode's codepoint. - Backspace: undo the last digit. Note that there is no visual feedback while in this mode. This is by design; foot's Unicode input mode is considered to be a fallback. The preferred way of entering Unicode characters, emojis etc is by using an IME. Default: _Control+Shift+u_. *quit* Quit foot. Default: _none_. # SECTION: search-bindings This section lets you override the default key bindings used in scrollback search mode. The syntax is exactly the same as the regular **key-bindings**. *cancel* Aborts the search. The viewport is restored and the _primary selection_ is **not** updated. Default: _Control+g Control+c Escape_. *commit* Exit search mode and copy current selection into the _primary selection_. Viewport is **not** restored. To copy the selection to the regular _clipboard_, use *Control+Shift+c*. Default: _Return KP_Enter_. *find-prev* Search **backwards** in the scrollback history for the next match. Default: _Control+r_. *find-next* Searches **forwards** in the scrollback history for the next match. Default: _Control+s_. *cursor-left* Moves the cursor in the search box one **character** to the left. Default: _Left Control+b_. *cursor-left-word* Moves the cursor in the search box one **word** to the left. Default: _Control+Left Mod1+b_. *cursor-right* Moves the cursor in the search box one **character** to the right. Default: _Right Control+f_. *cursor-right-word* Moves the cursor in the search box one **word** to the right. Default: _Control+Right Mod1+f_. *cursor-home* Moves the cursor in the search box to the beginning of the input. Default: _Home Control+a_. *cursor-end* Moves the cursor in the search box to the end of the input. Default: _End Control+e_. *delete-prev* Deletes the **character before** the cursor. Default: _BackSpace_. *delete-prev-word* Deletes the **word before** the cursor. Default: _Mod1+BackSpace Control+BackSpace_. *delete-next* Deletes the **character after** the cursor. Default: _Delete_. *delete-next-word* Deletes the **word after** the cursor. Default: _Mod1+d Control+Delete_. *delete-to-start* Deletes search input before the cursor. Default: _Ctrl+u_. *delete-to-end* Deletes search input after the cursor. Default: _Ctrl+k_. *extend-char* Extend current selection to the right, by one character. Default: _Shift+Right_. *extend-to-word-boundary* Extend current selection to the right, to the next word boundary. Default: _Control+w Control+Shift+Right_. *extend-to-next-whitespace* Extend the current selection to the right, to the next whitespace. Default: _Control+Shift+w_. *extend-line-down* Extend current selection down one line. Default: _Shift+Down_. *extend-backward-char* Extend current selection to the left, by one character. Default: _Shift+Left_. *extend-backward-to-word-boundary* Extend current selection to the left, to the next word boundary. Default: _Control+Shift+Left_. *extend-backward-to-next-whitespace* Extend the current selection to the left, to the next whitespace. Default: _none_. *extend-line-up* Extend current selection up one line. Default: _Shift+Up_. *clipboard-paste* Paste from the _clipboard_ into the search buffer. Default: _Control+v Control+y Control+Shift+v XF86Paste_. *primary-paste* Paste from the _primary selection_ into the search buffer. Default: _Shift+Insert_. *unicode-input* Unicode input mode. See _key-bindings.unicode-input_ for details. Default: _none_. *scrollback-up-page* Scrolls up/back one page in history. Default: _Shift+Page\_Up Shift+KP\_Page\_Up_. *scrollback-up-half-page* Scrolls up/back half of a page in history. Default: _none_. *scrollback-up-line* Scrolls up/back a single line in history. Default: _none_. *scrollback-down-page* Scroll down/forward one page in history. Default: _Shift+Page\_Down Shift+KP\_Page\_Down_. *scrollback-down-half-page* Scroll down/forward half of a page in history. Default: _none_. *scrollback-down-line* Scroll down/forward a single line in history. Default: _none_. *scrollback-home* Scroll to the beginning of the scrollback. Default: _none_. *scrollback-end* Scroll to the end (bottom) of the scrollback. Default: _none_. # SECTION: url-bindings This section lets you override the default key bindings used in URL mode. The syntax is exactly the same as the regular **key-bindings**. Be careful; do not use single-letter keys that are also used in *[url].label-letters*, as doing so will make some URLs inaccessible. *cancel* Exits URL mode without opening a URL. Default: _Control+g Control+c Control+d Escape_. *toggle-url-visible* By default, the jump label only shows the key sequence required to activate it. This is fine as long as the URL is visible in the original text. But with e.g. OSC-8 URLs (the terminal version of HTML anchors, i.e. "links"), the text on the screen can be something completely different than the URL. This action toggles between showing and hiding the URL on the jump label. Default: _t_. # SECTION: text-bindings This section lets you remap key combinations to custom escape sequences. The format is _text=combo1...comboN_. That is, the string to emit may have one or more key combinations, space separated. Each combination is in the form _mod1+mod2+key_. The names of the modifiers and the key *must* be valid XKB key names. The text string specifies the characters, or bytes, to emit when the associated key combination(s) are pressed. There are two ways to specify a character: - Normal, printable characters are written as-is: *abcdef*. - Bytes (e.g. ESC) are written as two-digit hexadecimal numbers, with a *\\x* prefix: *\\x1b*. Example: you would like to remap _Super+k_ to the _Up_ key. The escape sequence for the Up key is _ESC [ A_ (without the spaces). Thus, we need to specify this in foot.ini (*Mod4* is the XKB name for the Super/logo key): *\\x1b[A = Mod4+k* Another example: to remap _Super+c_ to _Control+c_: *\\x03 = Mod4+c* # SECTION: mouse-bindings This section lets you override the default mouse bindings. The general format is _action=combo1...comboN_. That is, each action may have one or more key combinations, space separated. Each combination is in the form _mod1+mod2+BTN\_<name>[-COUNT]_. The names of the modifiers *must* be valid XKB key names, and the button name *must* be a valid libinput name. You can find the button names using *libinput debug-events*. The trailing *COUNT* (number of times the button has to be clicked) is optional and specifies the click count required to trigger the binding. The default if *COUNT* is omitted is _1_. To map wheel events (i.e. scrolling), use the button names *BTN_WHEEL_BACK* (up) and *BTN_WHEEL_FORWARD* (down). Note that these events never generate a *COUNT* larger than 1. That is, *BTN_WHEEL_BACK+2*, for example, will never trigger. Foot also recognizes tiltable wheels; to map these, use *BTN_WHEEL_LEFT* and *BTN_WHEEL_RIGHT*. A modifier+button combination can only be mapped to *one* action. Let's say you want to bind *BTN\_MIDDLE* to *fullscreen*. Since *BTN\_MIDDLE* is the default binding for *primary-paste*, you first need to unmap the default binding. This can be done by setting _action=none_; e.g. *primary-paste=none*. *selection-override-modifiers* The modifiers set in this set (which may be set to any combination of modifiers, e.g. _mod1+mod2+mod3_, as well as _none_) are used to enable selecting text with the mouse irrespective of whether a client application currently has the mouse grabbed. These modifiers cannot be used as modifiers in mouse bindings. Because the order of bindings is significant, it is best to set this prior to any other mouse bindings that might use modifiers in the default set. Default: _Shift_ The actions to which mouse combos can be bound are listed below. All actions listed under *key-bindings* can be used here as well. *scrollback-up-mouse* Normal screen: scrolls up the contents. Alt screen: send fake _KeyUP_ events to the client application, if alternate scroll mode is enabled. Default: _BTN\_WHEEL\_BACK_ *scrollback-down-mouse* Normal screen: scrolls down the contents. Alt screen: send fake _KeyDOWN_ events to the client application, if alternate scroll mode is enabled. Default: _BTN\_WHEEL\_FORWARD_ *select-begin* Begin an interactive selection. The selection is finalized, and copied to the _primary selection_, when the button is released. Default: _BTN\_LEFT_. *select-begin-block* Begin an interactive block selection. The selection is finalized, and copied to the _primary selection_, when the button is released. Default: _Control+BTN\_LEFT_. *select-word* Begin an interactive word-wise selection, where words are separated by whitespace and all characters defined by the *word-delimiters* option. The selection is finalized, and copied to the _primary selection_, when the button is released. Default: _BTN\_LEFT-2_. *select-word-whitespace* Same as *select-word*, but the characters in the *word-delimiters* option are ignored. I.e only whitespace characters act as delimiters. The selection is finalized, and copied to the _primary selection_, when the button is released. Default: _Control+BTN\_LEFT-2_. *select-quote* Begin an interactive "quote" selection. This is similar to *select-word*, except an entire quote is selected (that is, everything inside the quote, excluding the quote characters). Recognized quote characters are: *"* and *'*. If a complete quote cannot be found on the current logical row (only one quote character, or none are found), the entire row is selected. The selection is finalized, and copied to the _primary selection_, when the button is released. After the initial selection has been made, it behaves like a normal word, or row selection, depending on whether a quote was found or not. This affects what happens when, for example, extending the selection. Notes: - Escaped quote characters are not supported (*"foo \\"bar"* will match *'foo \\'*, not *'foo "bar'*). - Foot does not try to handle mismatched quote characters; they will simply not match. - Nested quotes (using different quote characters) are supported. Default: _BTN\_LEFT-3_. *select-row* Begin an interactive row-wise selection. The selection is finalized, and copied to the _primary selection_, when the button is released. Default: _BTN\_LEFT-4_. *select-extend* Interactively extend an existing selection, using the original selection mode (normal, block, word-wise or row-wise). The selection is finalized, and copied to the _primary selection_, when the button is released. Default: _BTN\_RIGHT_. *select-extend-character-wise* Same as *select-extend*, but forces the selection mode to _normal_ (i.e. character wise). Note that this causes subsequent *select-extend* operations to be character wise. This action is ignored for block selections. Default: _Control+BTN\_RIGHT_. *primary-paste* Pastes from the _primary selection_. Default: _BTN\_MIDDLE_. *font-increase* Increases the font size by 0.5pt. Default: _Control+BTN\_WHEEL\_BACK_ (also defined in *key-bindings*). *font-decrease* Decreases the font size by 0.5pt. Default: _Control+BTN\_WHEEL\_FORWARD_ (also defined in *key-bindings*). # TWEAK This section is for advanced users and describes configuration options that can be used to tweak foot's low-level behavior. These options are *not* included in the example configuration. You should not change these unless you understand what they do. Note that these options may change, or be removed at any time, without prior notice. When reporting bugs, please mention if, and to what, you have changed any of these options. *scaling-filter* Overrides the default scaling filter used when down-scaling bitmap fonts (e.g. emoji fonts). Possible values are *none*, *nearest*, *bilinear*, *impulse*, *box*, *linear*, *cubic* *gaussian*, *lanczos2*, *lanczos3* or *lanczos3-stretched*. Default: _lanczos3_. *overflowing-glyphs* Boolean. When enabled, glyphs wider than their cell(s) are allowed to render into one additional neighbouring cell. One use case for this are fonts with wide italic characters that "bend" into the next cell. Without this option, such glyphs will appear "cut off". Another use case are fonts with "icon" characters in the Unicode private usage area, e.g. Nerd Fonts, or Powerline Fonts and legacy emoji characters like *WHITE FROWNING FACE*. Note: might impact performance depending on the font used. Especially small font sizes can cause many overflowing glyphs because of subpixel rendering. Default: _yes_. *render-timer* Enables a frame rendering timer, that prints the time it takes to render each frame, in microseconds, either on-screen, to stderr, or both. Valid values are *none*, *osd*, *log* and *both*. Default: _none_. *box-drawing-base-thickness* Line thickness to use for *LIGHT* box drawing line characters, in points. This value is converted to pixels using the monitor's DPI, and then multiplied with the cell size. The end result is that a larger font (and thus larger cells) result in thicker lines. Default: _0.04_. *box-drawing-solid-shades* Boolean. When enabled, box drawing "shades" (e.g. LIGHT SHADE, MEDIUM SHADE and DARK SHADE) are rendered as solid blocks using a darker variant of the current foreground color. When disabled, they are instead rendered as checker box pattern, using the current foreground color as is. Default: _yes_. *delayed-render-lower*, *delayed-render-upper* These two values control the timeouts (in nanoseconds) that are used to mitigate screen flicker caused by clients writing large, non-atomic screen updates. If a client splits up a screen update over multiple *write*(3) calls, we may end up rendering an intermediate frame, quickly followed by another frame with the final screen content. For example, the client may erase part of the screen (or scroll) in one write, and then write new content in one or more subsequent writes. Rendering the frame when the screen has been erased, but not yet filled with new content will be perceived as screen flicker. The *real* solution to this is _Application Synchronized Updates_ (https://gitlab.freedesktop.org/terminal-wg/specifications/-/merge_requests/2). The problem with this is twofold - first, it has not yet been standardized, and thus there are not many terminal emulators that implement it (foot *does* implement it), and second, applications must be patched to use it. Until this has happened, foot offers an interim workaround; an attempt to mitigate the screen flicker *without* affecting neither performance nor latency. It is based on the fact that the screen is updated at a fixed interval (typically 60Hz). For us, this means it does not matter if we render a new frame at the *beginning* of a frame interval, or at the *end*. Thus, the goal is to introduce a delay between receiving client data and rendering the resulting state, but without causing a frame skip. While it should be possible to estimate the amount of time left until the next frame, foot's algorithm is currently not that advanced, but is based on statistics I guess you could say - the delay we introduce is so small that the risk of pushing the frame over to the next frame interval is also very small. Now, that was a lot of text. But what is it foot actually does? When receiving client data, it schedules a timer, the *delayed-render-lower*. If we do not receive any more client data before the timer has run out, we render the frame. If however, we do receive more data, the timer is re-scheduled. That is, each time we receive client data, frame rendering is delayed another *delayed-render-lower* nanoseconds. Now, while this works very well with most clients, it would be possible to construct a malicious client that keeps writing data at a slow pace. To the user, this would look like foot has frozen as we never get to render a new frame. To prevent this, an upper limit is set - *delayed-render-upper*. If this timer runs out, we render the frame regardless of what the client is doing. If changing these values, note that the lower timeout *must* be set lower than the upper timeout, but that this is not verified by foot. Furthermore, both values must be less than 16ms (that is, 16000000 nanoseconds). You can disable the feature altogether by setting either value to 0. In this case, frames are rendered "as soon as possible". Default: lower=_500000_ (0.5ms), upper=_8333333_ (8.3ms - half a frame interval). *damage-whole-window* Boolean. When enabled, foot will 'damage' the entire window each time a frame has been rendered. This forces the compositor to redraw the entire window. If disabled, foot will only 'damage' updated rows. There is normally *no* reason to enable this. However, it has been seen to workaround an issue with _fractional scaling_ in _Gnome_. Note that enabling this option is likely to increase CPU and/or GPU usage (by the compositor, not by foot), and may have a negative impact on battery life. Default: _no_. *grapheme-shaping* Boolean. When enabled, foot will use _utf8proc_ to do grapheme cluster segmentation while parsing "printed" text. Then, when rendering, it will use _fcft_ (if compiled with _HarfBuzz_ support) to shape the grapheme clusters. This is required to render e.g. flag (emoji) sequences, keycap sequences, modifier sequences, zero-width-joiner (ZWJ) sequences and emoji tag sequences. It might also improve rendering of composed characters, depending on font. - foot must have been compiled with utf8proc support - fcft must have been compiled with HarfBuzz support This option can also be set runtime with DECSET/DECRST 2027. See also: *grapheme-width-method*. Default: _yes_ *grapheme-width-method* Selects which method to use when calculating the width (i.e. number of columns) of a grapheme cluster. One of *wcswidth*, *double-width* and *max*. *wcswidth* simply adds together the individual width of all codepoints making up the cluster. *double-width* does the same, but limits the maximum number of columns to 2. This is more correct, but may break some applications since applications typically use *wcswidth*(3) internally to calculate the width. This results in cursor de-synchronization issues. *max* uses the width of the largest codepoint in the cluster. Default: _double-width_ *font-monospace-warn* Boolean. When enabled, foot will use heuristics to try to verify the primary font is a monospace font, and warn if it is not. Disable this if you still want to use the font, even if foot thinks it is not monospaced. You may also want to disable it to get slightly faster startup times. Default: _yes_ *max-shm-pool-size-mb* This option controls the amount of virtual address space used by the pixmap memory to which the terminal screen content is rendered. It does not change how much physical memory foot uses. Foot uses a memory mapping trick to implement fast rendering of interactive scrolling (typically, but applies to "slow" scrolling in general). Example: holding down the 'up' or 'down' arrow key to scroll in a text editor. For this to work, it needs a large amount of virtual address space. Again, note that this is not physical memory. On a normal x64 based computer, each process has 128TB of virtual address space, and newer ones have 64PB. This is an insane amount and most applications do not use anywhere near that amount. Each foot terminal window can allocate up to 2GB of virtual address space. With 128TB of address space, that means a maximum of 65536 windows in server/daemon mode (for 2GB). That should be enough, yes? However, the Wayland compositor also needs to allocate the same amount of virtual address space. Thus, it has a slightly higher chance of running out of address space since it needs to host all running Wayland clients in the same way, at the same time. In the off chance that this becomes a problem for you, you can reduce the amount used with this option. Or, for optimal performance, you can increase it to the maximum allowed value, 2GB (but note that you most likely will not notice any difference compared to the default value). Setting it to 0 disables the feature. Limitations: - only supported on 64-bit architectures - only supported on Linux Default: _512_. Maximum allowed: _2048_ (2GB). *sixel* Boolean. When enabled, foot will process sixel images. Default: _yes_ *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 *8-bit*, or *10-bit*. The default, *8-bit*, uses 8 bits for all channels, alpha included. When *gamma-correct-blending* is disabled, this is the best option. When *gamma-correct-blending* is enabled, you may want to enable 10-bit surfaces, as that improves the color resolution. Be aware however, that in this mode, the alpha channel is only 2 bits instead of 8 bits. Thus, if you are using a transparent background, you may want to use the default, *8-bit*, even if you have gamma-correct blending enabled. You should also note that 10-bit surface is slower. This will increase input latency and decrease rendering throughput. Default: _8-bit_ # SEE ALSO *foot*(1), *footclient*(1) ��������������������������������������������������foot-1.21.0/doc/footclient.1.scd��������������������������������������������������������������������0000664�0000000�0000000�00000014067�14766001452�0016324�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������footclient(1) # NAME footclient - start new terminals in a foot server # SYNOPSIS *footclient* [_OPTIONS_]++ *footclient* [_OPTIONS_] <_command_> [_COMMAND OPTIONS_] All trailing (non-option) arguments are treated as a command, and its arguments, to execute (instead of the default shell). # DESCRIPTION *footclient* is used together with *foot*(1) in *--server* mode. Running it without arguments will open a new terminal window (hosted in the foot server), with your default shell. The exit code will be that of the terminal. I.e *footclient* does not exit until the terminal has terminated. # OPTIONS *-t*,*--term*=_TERM_ Value to set the environment variable *TERM* to (see *TERMINFO* and *ENVIRONMENT*). Default: _@default_terminfo@_. *-T*,*--title*=_TITLE_ Initial window title. Default: _foot_. *-a*,*--app-id*=_ID_ Value to set the *app-id* property on the Wayland window to. Default: _foot_ (normal mode), or _footclient_ (server mode). *-w*,*--window-size-pixels*=_WIDTHxHEIGHT_ Set initial window width and height, in pixels. Default: _700x500_. *-W*,*--window-size-chars*=_WIDTHxHEIGHT_ Set initial window width and height, in characters. Default: _not set_. *-m*,*--maximized* Start in maximized mode. If both *--maximized* and *--fullscreen* are specified, the _last_ one takes precedence. *-F*,*--fullscreen* Start in fullscreen mode. If both *--maximized* and *--fullscreen* are specified, the _last_ one takes precedence. *-L*,*--login-shell* Start a login shell, by prepending a '-' to argv[0]. *-D*,*--working-directory*=_DIR_ Initial working directory for the client application. Default: _CWD of footclient_. *-s*,*--server-socket*=_PATH_ Connect to _PATH_ instead of *$XDG\_RUNTIME\_DIR/foot-$WAYLAND\_DISPLAY.sock*. *-H*,*--hold* Remain open after child process exits. *-N*,*--no-wait* Detach the client process from the running terminal, exiting immediately. *-o*,*--override*=[_SECTION_.]_KEY_=_VALUE_ Override an option set in the configuration file. If _SECTION_ is not given, defaults to _main_. *-E*,*--client-environment* The child process in the new terminal instance will use footclient's environment, instead of the server's. Environment variables listed in the *Variables set in the child process* section will be overwritten by the foot server. For example, the new terminal will use *TERM* from the configuration, not footclient's environment. *-d*,*--log-level*={*info*,*warning*,*error*,*none*} Log level, used both for log output on stderr as well as syslog. Default: _warning_. *-l*,*--log-colorize*=[{*never*,*always*,*auto*}] Enables or disables colorization of log output on stderr. *-v*,*--version* Show the version number and quit *-e* Ignored; for compatibility with *xterm -e*. See *foot*(1) for more details. # EXIT STATUS Footclient will exit with code 220 if there is a failure in footclient itself (for example, the server socket does not exist). If *-N*,*--no-wait* is used, footclient exits with code 0 as soon as the foot server has been instructed to open a new window. If not, footclient may also exit with code 230. This indicates a failure in the foot server. In all other cases the exit code is that of the client application (i.e. the shell). # TERMINFO Client applications use the terminfo identifier specified by the environment variable *TERM* (set by foot) to determine terminal capabilities. Foot has two terminfo definitions: *foot* and *foot-direct*, with *foot* being the default. The difference between the two is in the number of colors they describe; *foot* describes 256 colors and *foot-direct* 16.7 million colors (24-bit truecolor). Note that using the *foot* terminfo does not limit the number of usable colors to 256; applications can still use 24-bit RGB colors. In fact, most applications work best with *foot* (including 24-bit colors)). Using *\*-direct* terminfo entries has been known to crash some ncurses applications even. There are however applications that need a *\*-direct* terminfo entry for 24-bit support. Emacs is one such example. While using either *foot* or *foot-direct* is strongly recommended, it is possible to use e.g. *xterm-256color* as well. This can be useful when remoting to a system where foot's terminfo entries cannot easily be installed. Note that terminfo entries can be installed in the user's home directory. I.e. if you do not have root access, or if there is no distro package for foot's terminfo entries, you can install foot's terminfo entries manually, by copying *foot* and *foot-direct* to *~/.terminfo/f/*. # ENVIRONMENT ## Variables used by footclient *XDG\_RUNTIME\_DIR* Used to construct the default _PATH_ for the *--server-socket* option, when no explicit argument is given (see above). *WAYLAND\_DISPLAY* Used to construct the default _PATH_ for the *--server-socket* option, when no explicit argument is given (see above). If the socket at default _PATH_ does not exist, *footclient* will fallback to the less specific path, with the following priority: *$XDG\_RUNTIME\_DIR/foot-$WAYLAND\_DISPLAY.sock*, *$XDG\_RUNTIME\_DIR/foot.sock*, */tmp/foot.sock*. ## Variables set in the child process *TERM* terminfo/termcap identifier. This is used by client applications to determine which capabilities a terminal supports. The value is set according to either the *--term* command-line option or the *term* config option in *foot.ini*(5). *COLORTERM* This variable is set to *truecolor*, to indicate to client applications that 24-bit RGB colors are supported. *PWD* Current working directory (at the time of launching foot) *SHELL* Set to the launched shell, if the shell is valid (it is listed in */etc/shells*). In addition to the variables listed above, custom environment variables may be defined in *foot.ini*(5). ## Variables *unset* in the child process *TERM_PROGRAM* *TERM_PROGRAM_VERSION* These environment variables are set by certain other terminal emulators. We unset them, to prevent applications from misdetecting foot. In addition to the variables listed above, custom environment variables to unset may be defined in *foot.ini*(5). # SEE ALSO *foot*(1) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/doc/meson.build�������������������������������������������������������������������������0000664�0000000�0000000�00000002632�14766001452�0015461�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true) if utmp_backend != 'none' utmp_add_args = '@0@ $WAYLAND_DISPLAY'.format(utmp_add) utmp_del_args = (utmp_del_have_argument ? '@0@ $WAYLAND_DISPLAY'.format(utmp_del) : '@0@'.format(utmp_del)) utmp_path = utmp_default_helper_path else utmp_add_args = '<no utmp support in foot>' utmp_del_args = '<no utmp support in foot>' utmp_path = 'none' endif conf_data = configuration_data( { 'default_terminfo': get_option('default-terminfo'), 'utmp_backend': utmp_backend, 'utmp_add_args': utmp_add_args, 'utmp_del_args': utmp_del_args, 'utmp_helper_path': utmp_path, } ) foreach man_src : [{'name': 'foot', 'section' : 1}, {'name': 'foot.ini', 'section': 5}, {'name': 'footclient', 'section': 1}, {'name': 'foot-ctlseqs', 'section': 7}] name = man_src['name'] section = man_src['section'] out = '@0@.@1@'.format(name, section) preprocessed = configure_file( input: '@0@.@1@.scd'.format(name, section), output: '@0@.preprocessed'.format(out), configuration: conf_data, ) custom_target( out, output: out, input: preprocessed, command: scdoc_prog.full_path(), capture: true, feed: true, install: true, install_dir: join_paths(get_option('mandir'), 'man@0@'.format(section))) endforeach ������������������������������������������������������������������������������������������������������foot-1.21.0/doc/sixel-wow.png�����������������������������������������������������������������������0000664�0000000�0000000�00000352242�14766001452�0015770�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR��6��&���Md���bKGD����� �IDATxAș%̍4-MBB B7=,04 2}Cԇ>Y. 4$44"[Bi+bdݒt 3="*?Nt{ggON8N8ip 'p}'p 'p':N8kEp '5ʼnN8NEs{_"'~1_{:|N8nuo :p ' OEM4ʤwTB~W:GO)o?5v ?7wc/?NxqDEň[pZ7^]>ho?uxkgK79N8wKͣvw5E~Fr:DHoGOvp '5*Y5=N82M({d%B_⹽!SSoeO8|Qzms|1E`b$IN9N8a M, aDs⣽M͏gutük'p ՟/XaBzN8&x@ߩ8]N87 d8]N87ė;N8ᄯJ'p '|MqN8xO>N8NbF39N8=}'p '|MQO~?_uo#/}/ŏyk_OR_Sz '|ɕο:ˣ%_oO~G7A6WARWqcgK<}?wŏL]xs{k£v[}y{U7yk^9 'EU]ᎰNnq1rElBZawݿ#6$Mb[&~VG?(})la98]rYM '5:ҖCr:x%:=8O"x#vϣl_ۄ[пw!!w Cg"CG{뷿S"\X?ߋ)7~@B_z k%.&> u;ߠ(ԟ~{aob&٩#|:(o~bNmEmqs0o WuC‰n_PQ}S$ '\d/ o٧|W}>o~Z5Pwr*c^G| ' B>ޜZn*~ήes=ě{ޅ7a6MXo}|1[Mݏ7<Oj8nlC_39wߘ'|y|39 '|q(ꛁXo2(t '!NN88>>)�Z2. C+fof�Zh&[i&pֲZ [`C.k[N\.% gXē1EQ*=_K6t.^9kcAT]i:�8@bU#�X�`,r�纼i+ڷZ1kPXr&�VM;gCx9͙u`jE3\(m GFxk�LkZ9S 3@kדo)!7J%swN �k[{2)S&RGP& .A1f�:&Z%xzP1`{0\_A/2�p�pjDe �*EY9Z3 qSXUWF9e`:~=խk5ܵ55 x+Zj|.hpyR„hr# $+`2®��h�Iڱ 6ޚDCkPu5Pʤăִ�$C`(ac:3gj̜+hG*a9y5ިRKF?>/Z~,w BRjzqt"z2m;�sumMc/_5?hm\lZ`mD^vfC -�ԫ+'fLdB @1X?[ؕbC9:Gi=~ir{=BK0r:dŸ�$ #ei12@۔버(uⲄڞu@!RU:� om`UF2sMI��~Ƀ>޿-6MvbUx'(izvG-_-8-=!@ S?Sl�@ʟsqklЍ`C}Ӆu@,w ;\NduKu;~n];0 +xS 9JƺX'|hLHו}bJj`h�y2l0Y^7KH4ڳ6׎v #? ~b1pE<*j,r�ӈ#+ 93@`DTDKC*Κ(�Q uk8BQm[z4BöӒ2lK_׶-sC7w�11 Rg_m%�Xn.ְJr�t@ֈIa=HjuΖ*ieYl7nVV5 =3j:/�[HM�lxȉ+3 �W�0 ))g!<OhaT}�/M{u霷VV͠끅.$4V:^π>_7Gt;#Ħ�hI_fr'u $RqPC?6$tR^N%܎RFG3yC ]h9H>ݚ\(A=FqcS@T` H6K  Vh] �Z55pcR ONtjx6{],vrׂ�/rş* �ޘw<K�Pʄ9%LJ=V` T]4v HR J Ma\ (h(@4-Ʀ)ܰVaH"WKѿ=l}G,Rn+&xЎOfP^ݘ"c)Zsl!bEo Q$tiǧ5uܻ.8͊o@;X�e4]*D.0I-oUV,} I]J P|܅Q#HfDPșs�(KQZ̛r�8J$5RBgTKi&hw98b�7쿵<9y: *O0Fwa7Q!e;tƒh@-5ݚƉ"w* Q1'qeA_d%J:!i}G``ȽQfiS�:uzǂו(}L)KH Qd-kFqI)rcڔ Ehp@c0;~ s�4@1W̼i|ar9QTcԡA A^:,ޭ^QR#AWVHTqDn3f�[S�Xk+%LX%ʴT͍ٴ$hbj 1hᾣo=H(Z9yފ)r�D.*WQnMtEjr [HEvIMjV}G4YS ^T]G R=0�]2kZCw-ttJ`P]e"rᬵ"rvZ8Γ5DN!Lj*8" ^. �PI3BP/&�9n 1mwu։й>fjtQ<h]u[>R�v5PI%P¸@szA1KaS0g+Tn{"1 *Qz�zPlJٔ2@?R`[jFvhr0||t#(u_,1N(`ss~) KqB}ł-uA8NQvBNX#xXgI{*⪰�X*\o+mk4D�ت/Թ^z"XC~"N QI/'VQie! 1IbҒWB-SZ$I99p�2dda\:"C!ʷ+Ây!$(A(}Tp9͠0%�(N$SLZ#DHX'hIows_. ,|b+S)} ) <;\WR{X"amiWaZuǓ#:ɖBjZhunkKB[m36c=tӋ@וaj�eR&Y.<UOeJ%TmnͦUIm� f@K#唃 hc"9h/F>h,sZFOfl8y(a/&bRP ΍q,s;/Yo88km~#=IU2F,ř?ج"a7< H,+}.DKbd2KT6JcRڈ>9f}7RUE i[ Wrw r8Vq*\A*+Âu t;^P#ʾow#"5n9E!RQzQaWlb\9ңgRsWČ�awD{DQn=%�\E�O]4ŧ�Hc�K։+T,lڟ9r~QW- 9:|JI$h=H(RN5kآFRܯ{22ACWS{kPL<C*cD>WQL\vnz]&dDkxgFr XuhD?i�-zۨ,v}Qe@N+OwZ/h;+[+:Q]\)tS2I'zh)M%{\yE#-xq0Z^ D Y5V["8Awh4�v8ls:m)ļcrj[O"N:B>9ҰLd='`N_hy۝:$Ybݮm0G@k dk5}ԗ .>9*'9 h?Htו r;g6�3r9Z 邹kLjPu>G $"\\|*vW3P˙9rO`J0Ғޗ;Gya#墔sT-sx8rz%*Jȩ~ѿq"scC'q�`Gd\ŅvRh.XSxЌNMS$R}RvɤެFV`-o*-a\Ɲ8g5)_dPvUkSp\˫a8 tZa]c/vƒ"^Ȼk͡`N?ż *Щ"vMsrL~�HEQpؽ=b}l Iׂ:AB& ֻ�U!G}tV+Q>1E} am\"$ tm b<s|;OϑY�riBL&�`Κ @i:ZVJ:qR)}|,Z-h[*Zψ  1B4RE繚[ Ki@Bsd~w5:NzZB=rKw"%{]oHe$=-ixAݏ|O@-v"۾p?-1]IN (n:6(O RTR*W(02%IB;H46BmͰB陋@Eh=Βj`tyފn )6Lal{z$N;@l4mhC,T؁VĚ U{+P$EE`CM z B.%zu^Ki@.RkcK/h$1ĤA'  $/: +HK5! /(V֭H6MB|DnOq<UWI%đ@i]FޚC,R,퍈jL&((3[ƨl=II]qXG6u1}\` .L\aȬC+TPQm OTcb zGkcE{&**_$$9*p.M'2N.o:^er@J8Q@c @*k 0˦AYc b6XH-~18=ҫ>HKU`c6|Yżfh"l#*N@1"~D㔔"95 ‡ׂ$ ] dqeFM 7 b3PՀ3%Ƒ@fFG ~T"[wp(G$ZA  0H(N??vet5t,QP�U'qă8T]L=QT$*G]/HYг41^wǾ,J >-N]K^n@r Zxh[+TTj7PαrYæ.ㇻԡ/槰ĽDu/&*X\KK*6G3y6EKѿh8!QjFe3hOhs@-\4rqi !89ҜYnlE8 / �[JPVHQ]:DQ 4A>7EECv9/2;e1WRq7̏ޝXa 96u* Q>HtX rQ?,m/N{hX26B)<?i?FRH�T+H l\MAr4'E^`6̆l~4yB9.9X)F*~ -W~UYdPM9RQZByC  ^<+5kw�zCFЙaƻhej;ΈW/ K}*.Q+PH,.[~xv"4`V(!(.erCә={4�h̜,f�^? `ʏeQ 3:lՇy$ 1 D OIBBRbI\xۮӻ]^*rpXF V=FS!RQ|F7ǒhop�94WdY6q@4({T庥WwC- N1s$(5`'�*#JR-p66RO�h|PRKVnyr*)�)-8PطりRQLN[A-<5N.觓(c K21pɟ\2{THŴ;!D_t$SQ##RqՀڸGr-с#&&QUHJ3v OXɍ 2GEDu�2O!XTv;�-Z9G3s8|,' #{Biʬ4R=I*Ș1# ` /bY(X y EI1ܷ BK =# 4Pkt5XOGd[)q7L>1T?"^Nq@``zhuR;JTE헅E F CX*b.a�rQ6ZMdO#'u: KV�� �IDAT2)eKY![%%,c�q.DPQی*x4#D{/9q;;f=fuJN=rA)[BE T:cS/ sq ``z'^]QO6 #]T66"97)"/Ч訩^ITj1|%.g .sT�4jCK.[ܐ; K6! ;)SR,%-.9)EHS()jc)"l:K #~pJNt%VQA`Mϑ>B@C[EDahsDN>%�ܘ55z钆 :^׶5^%юc;RQU b`:r I򀉗{+3 A(nH䊗Jޡw~5%ɘL[.C�0(G @S[JDK30qDԐ!G c~ 9p 9(ƴU^M&U  p%`SM-,dRI+䪈 Yt=\ԀnsZaԪ.sR3'RƘñGF5Eɖ>~Ťl*ykDj:;Tf `Tib䒠AQL蕄[S/U)6�h@*yg$x]<n>z*4\KS c3M]l^B1ĻBjIor]ـy|X;P9H9Rq^6:7}/0{ю{ ֑IM㡡(_=` 5Yp.ca9RRV94 & ֻua7i2Sd(3*70 K'8D .("b*-D>C*&:Uꡐ@] |�r^wR\GV?kk kuΡ9Z2OT-h%ՐJ"* 5�hݺk][k͝OP;{OQڸ::X߄bf ;{N BI�{�J纄XBYt"/ΥMMn0}Ppc!iCZQ"~Th!Mn8H &c(GS h,ID\}aK *VB9g:_d^(L/!.E1OZnVűԹ>)C8-:ɇJc)G%V!gURӔ(**TEEZ!?7Nl6K$5*jd0y66"f]#RKf2)=�e׎QF>�h"~"5J'⽭0�U1`Jr $To4jdkΗӸ\5aj%@EN55P<Eά:1%B!zƿ-ra֏98?[UǷۍ ]wqd:I8f;d^PQw0G١zղd/.3^?t⦭HH1)+1ndl~�H�,\e'ygk(a}u'EcL6?uI`_B5v Q:*\=SOQQ` %DpHTG~%R czտPYR*M3[8>BPR5,bO%ՐyR>U7!Jþ<Pa2RzL7�dHDEU."G"ɤFb f@*Q`òDυ ]fw[Q$nꩽ@wK  5BNxI[tF 4}2;N+9,%1`5˕jNt΁B)`.!xZh=$ĐfcI64y6!UN-c DE8hgᾣT2a/V Kۿ\Wv|.\כe*G%+�uj[PA(z$O 6QOVE-�Rjw_pXW"Ny3mD{sud�EkIN-Β!GCz'yع7=}E&_T⁴T:Awz?1D{|qA%C4.�!fDr YuKWW&n5&0 jH `rrQ6ɸZ5d0JW`\+׮-*CLЯmd;K\"(�� �o2UhQoADx,؂.w3wL-XRxmkV Tc1 Z1=V Z_yI#*ɕ sN ` �h];Rj0dE$ۻnjoO L8tn.aCO_Hf5W25]@"KB@Bz$'(M5nl% 90L5:C&ՀB 57 j=sYb"f+/L憟(&DE+G|I;TZmP.]P7e*Jz]Cq<J`U9�^ew钋KE4uf>:nr݁B9x.F"CkXH2%PrEժj6e;a],%̱|1GiGTFJ-G&4rF ˽>V z%ځFs{nb]"I,\+jDI5&TTV5m5 mmͲJu/y6WCTnzɸ䔅4t#wh[K(n]0@cc '/WXjGC|Nnj5/Lǯxbx0K+Islq=Ɠ+dUBH?#?z VKZ=~� ?`y{!G;Ltj GM],_|jGVnGMM& {AHp8@Ӥ dB^IE=GĬK(�z*0dPБ!idw'B>^07fZ#爪"M`/qՏi[1ifh?`V�rQRrQtf@u%hiVHvmi]mj NosTihA$B1:Jt#ŅeJEb"lW߹,%E3q# R10h9\/5{xi`mV7͡uѰ^tcvf0>#hL(>'KMb}�. 6+eBT.ٹl|a�C4CxjNt"5Fƚ_ ɸC%dIWl ]O2wA(Sԫ+ګ>8))PH*7/f} ATwXiXNkg"XjXߞI:ƀr6K,  6P=_1=bKK*JH?tGjW'cgFʧ=,G@tO Ob+ lk׀elTDN ЍeOnaYD<MDmv BMf' �́swG|+ #2Jx3,nyQ zT*X??Xoæ?phZkAhZ,Oo%{6T<^CuTvL][N6QiwɳVl#s۳n߻PMw/O&UIf̞] m26:6"acTϾvP*6Vh鶩<3L5MY۸TKFTno�gMbx{J1X6Nu=Ĺ糡<a<a\rqoS1I6v|v� /)X'o_ٙ"ߺ!Ù{>L,A`н0pkN="Mh7ζݫY-yR>O�l8říegB+gn}n~e)�!-2�H-n Vmo~MfaW̽Dgg^|jVʣ_lIfm�ݺaKs\`v5&l6n٫d_3n5@?(ZAjI3)u{p㞡=H?9SxXʚW{n_:ރڪ<K;gRU4@v,Xlb!jU[Z}hybuڛ /a_͈d3. F/x'-?u ;+ b;C*3@�Zea+f[;l0`vҫ*vk.Rl8$b+ok>RlYp `;@XnU=h4]X1:,bmlω+uu-YQ\#/]�++\ �9B11�F@#P9RQ9Blw(H)B-8PT'PQqRX<%ȟ`,+$jcMt2<T~̐褘sA-wwϥm2d7Bz c/)  Ht\U9"mD JJ#TLW~J 9 )tr)ڲ%0/2E/qnRPasKf){u nb;qt*;ԲK?( 4+OdX‡HB!1=j0Oe䕠 ȶV2C]C'ڸզZ5Ti " oJ}ubɵHCh|ӡgQ/t>}b0}K*j<<(�m ** [�XdMU"5=<�fHgg<KlŅ+pA X%Nw۟qT⬓=�t!eZ^WnPԨ[z~Tԫbs[FP?ڳ cm9n_/b- #R˂_C۸/Cds&f3[;regnhg=헫tn:}F J1l?)l9f>A'u@$ �fgu:�5ԫ n=;ۭ=;{8sѦSG#j{F"I`GlՐ=f6Ɍr?v˂n]Dg]gN8�qR .`o:KKb8RQ&3"rV!e^E%܉[_T@R�6&q֩�%oO^m3Ѿ�HBaL#afAٖ8TK[[t[`ӝ*/ֻ0n%xV6ۀ)xgL[UTPQ$~X:_f3OjY?Ŗ ؠa2L: ;il/6͆K nK5gM5?8�^ 0~/a<oX$ܸ~g#pLf3ř[,͖m n;㱖%~3F+t6 9Rڭ1�b `YZ46"6lR.nH/t`.URg\d.L%omD-G <̇tEBPW\;?W W <GP�`?Y3K.^~ZBv*I 8]p׌(MWpYpz3u. N8(ُ|a].Mp>Q[O[ɷBtQ >Լ _ Ue&b+vHz-++RVljBѐ`أMx0:dF[$k}HY<7奞]g+ǎ؍B!0)(- �� H6 Lcv*䟎J⨊,sq4Nl(ZR&E#hZSk2Cph}­PiZK*DqJX'.] AOXR Bd%+�oY|}8 B&_&R.j`ߝd A'L0y']%2gsJd\DM .E E"GPC;@ҭ)s gFQrbY~ ӐEP)?~Ip&h EhI**D&A -E�Js \&UAHMN~gV�*L^QhoDTgmB "TRWSď@䤜Jb}qыƚ"q-+Z! !>_.&9;07|*G,L4eF=5 ޹lEVG *yΦ'?7))U|' 4:j{8C|~M漟 x@W?^R -PI壪Mmm噩4̶�\DOB$+ i-c g,uZXRQAԲfrŤD*r;1FBCFuGi#RAI/8q@Nx & e,1qQl#Zhx\w?͍]9:}z+�JKra.21`./1Zh<Pa> wO(%i>vP"C J b4]Bd DZtÇcRK~8taLDі�c*~W1ZTt1`p�\wvft�3JhuXM'T)_EDƿ$;_y!~bxx>$2ۻT0IB?rj*4|qY -f)fp;٦RD& K`Rgs<GΣB}{1b'i$-8.@C}~N\*0{Ud@U�n\=⧔g-z'HoQ# whZ3u ;N*.3l2G%":>PJW!#3I%Th׶][d-.(%l@8%ǹf3)Y/r۟ō} QB{]纲CtsSjQ!Ϳ1l_9JO]-z?Bb\\jk}.d;;p�jccGFQ\,hW/1PZһu�ظ9栜s6P{O{X pYp#Rs�u~ϻLs'-E'e�{>XA,Q`QŊU=U >Ɖb}dե=@y8LJʰ؇J=d% V/PsJ +)"El-!_| %!EDS3p�/싏o6^-;vǤ$]\<�6j > bU?y8QO**<Y[1OTiZ+qED"^1Y�Q|o2G%-ro: #tQ^�ڊm9ht-R%s p]fpK&Z%9tmӂ$� sq]WeGsCqH{OsQO˜OZ'bK |$}Dz]OR(qVԥ Zܵ.2PU;B=ҕ.( Z ;;h>8W�<1W+?s^)WhRY,FǏ�`Q嘹�P*j&A ;n,;O9(٥vሸ<z')`]!/tOLBh;�#y8.ɯ@KOd !�H`yI3ꖟb~ \qQ?�Q25n/@\ArW=~B4քzK\Yy@ ϵ~m6KD�� �IDATB \2ei^*)uI�\g� }?`4h4|1'!eNR"[|@Y>i#�~И g;\eޢ eL�<8(rQ ~1z{q&w~ᵽqC0偂.g!Șr=x=9œEKDAE鏶x$F+.kz*[\Mҭ6 $p UB6:H1Rf�eՕ@'E%uݢBc׿\Z3) P:&8,BMHKõ^<0+ pו]|ڕ/Y/C*ws ^>IE5.*L X.A2M\nX&a';�7|ٿ_q�Pdyaqfj26�F9�5$^e$4i Iu+)R)^VbfOD쪁j]**NTCTS^ˠuΜ|pEdlXc.rY4q١ Hޘ´Oo?OdQaOږKQZnuk|%�w{~OR�_܁)ۤi1Uw_4/judE' y,WorM1kjO_TՋ_3TW>_u! f~oZ:4$@YoD a=OAEaaTQxm (zn$x$rGͰWPB >/'RF{obBKʉ^"dP�Dal3ԭ9UUu.jhzW�T pHQKz8(Y(@,_Tl~eد_SW:zn(� ͼk--Sb#A27|&[ɏngXChOA_(�k'd)H2\TZ/%�XNT/̓1W:DKQ#Ko!JB5"f!Ƨ6Izn<@ 4릜=Fv�t(G z%)� 6,,L':wb^' 4�X&Kj`Xǥ}o>´O,>rE mb�Qor;F\qNEk�kk>+-g-b}Bͮe h;OScڬ,YY*M=Z|�*?L?߈Ӕx^}'W1, WY%,jpW@$;W�R7\UZΏn]"uJ`#ʇc3Mf/K?r3snRʪ`JKG3ᖰN %r/s.uU141S(1;k�I1W`uⒶ Eaɀ|�Pf�4{mjv?~6^X?X C: ~,{,{mVc_�R[`]~~QGE}L%6cizN3)\s<6@M!ev}܀YʶMy>t_ֵmGawV?߾7`1{lV>t\"IYj8Օ;.φagpv_B7|l ~6^9wíKaR~{^p�uKZLdnd"[`@nl"7Y"l� Dvƍ#6L,86¿vfoi r=Mvϰ0P7WW٭ W[;?F=;28W-Wx?uYy-mP46g]9,w�Hdf%+YdTd$ni5l~[ݿbvZk=^4(]|dXU@<=}?\ URk۶ss(�{ύk]GRzEqAE?Em/7_?}qPR~M'Vǟ~yca(N�X!JzYJޱX@el7@@�q/_�6[QĄyxZHv‡jwڸוwtV6nׅh�XƽpnPt˒}d3ñ9^ַmmqbӞy#w7P�aɩX ܤ O>,0דZ9D!pg/t<zc20Eϋ^;. #USޝY bsdc 7hrڪ)�*T Ž*b*ґ:. 7.*�L#o; 3UGad N4 !S8" ^;m0!|r'tb(Cw'CB2]1Cvx'qbz%Ib*;̷{{z]\Xgs'�j_3j;tT%t\gGIn'Qi9wat6hWۥ_v,XUl] <畲r�qT J4[a KKNBr�_.-,dq؞+`P�i{J t>Y3BZ||+B#zQ? ߉><a6c2tu ڗ5`;e`,1a-� 52yHժPO+$ܟ_LJ~tc^OW܋Ͽ׿:A]_OOo߼[߹͇glv88h-s2"FIJ{G7�nJtkmhq^E>t"ږG;ӧE}_Yeܷ�ӋO5>e  `ac��AQ cW~r@aIk ]Cˏӹ钐v$`Z$ѤZVQpe 36uQF,~z3vM(E9 p4fA+xf(+"֑#0s `#mB Q7d+1AeC1RLP sOJMFPXfPd>_]Wͼ6��Z�Qx9 HXx`ij( 0և!9R_ݑ�04ʾRG!йIi藁c+4qeB;m2$ٝNe8pI]Ie,*n;�Z`25DEPk{Z;7Br댋%=V-J&Xte6P7س=61*gd,}YQ @EPjXl(pLkC&VպcU9D;.0Ws& sZPC+դ ѿՂס Pzoyv؈W$X#�;t/<{Wc]nͿ8??{_|po~Yyˇ>WLńP*Vʈ AEo*%uf"TV[o6]m\ Ƌ"+Q*rWwĢC:T&U⭋9P�`Pidkd8jK,*oCW)qcƐ6I1׃ļF^�KoO D]l/dR8y-j<JL!˯�%AմwDq3&hu 9<xmq3&|fZI.4 . 9T-.VW7HUG]s ׎%ޯvv0`9 8'L'"r3tp=HtL^'%(�jWR&B2b<YB"D:b{bi.ĂeUG`e^\5^#8p<s%]EA/xw7/<wT6P~ED3 z�}ݞ֟VY_G26Җ<!zȂgYZGX7 XTKPF(�\p�^,l3< {iMS3yjv0Y'tz8c<ZcWEPs= ۥ{pROh (]-QbA;D|KBֱ`{n)'a<Gyk~ �Uu~ɧ/~Ot^]͗֔jPLӺӹhzx1�0FiabBP̩i4-bBlK$Etw;URVYYpXI(ҼuvVB;NymOK ǖ,L+F',*b*{EK_G=Q٣Ahoo>IN0=`Ace0j�C"'XOHT˥`U'H4 %F ,>粒WYU&�S6.O|o8l8]Ҹb!Tdy/"ЉڱIƶ/6Y774֊H�0 Hߓp  D!ab&"@J2G\`Y {�OLuoμԄLKWL` xجgPDα 0"pb!Q(Yzg9B{DEhO|޳Aɖ+ uG8 4OaT\HR�A༵)zς O4JW�A� l# l }8m6Ti  _MI5硗и)|SyV<bzpMc5I[y"bη%oө9wu( D诜yN8ʟ&ef3/NMx: Įʓ_~[O?ΪL�x ּu5M;_q5 <rTCjh vy:9٬zrnq4r8[u6^GPrG2Uc?~-wGmWO]gA �:[g!Iy|UW_ثWz̧ (MĢHc#:="|V.)~26Fy/ji)WS<..θ_`X|Z<=mV6;ɝ�R<a$Ke0y+yݼA!11q6c@a#h(qemҷgBmٍW[H.|�<5E,fQC E :]48dQU�?Vҏd.I.l"s'"ɓ@�]G|@$nN]|oo_vl%%:ccBpa/V'˹&Y=.SUb~o8 tu88-<Pw]h<6BQC Z}&$d"C,@O bQe]Xk�Q\!(ECFDj0 |-! +�IvV0+|з~ǸIT|Zr=P }aC?;> |hS33~=?sWW( dBU;/c^4M~u?I*ϕ57Tqn)0̗P 1@Z�к;=wnݾهO?}OnO߿ɗwntv2ݨ,,(aL2=|ivĺ  (Nm kϬںO@\bQA 00Y=K5h}Q6&=149'4>Yq1.x+dTp%B,åXč*Z8S($!Dw<9Dh᭮;/b|IF؍b7Ly (F>+(.D/ĢDCm"ĢH# E|ajODʈ!x^}  kWF]ʨqu�j�]bxZ;O:w{nh ^YmW}+<z0Jq\#njl2wt@S�3Sܡq\F];5CTWLt#PUP�<n1.~M^\tnpJFҨߖ>]<mǟ.N֘fޛdo{b:YqvQsņ)Bh�kHBKB!^Wn7;/V Os .P E4Q4}>oɮfTջmNMeV_2sʰHH%n-WqF \XPYM3M, i]͏Je?:gG*srꈭM3�|:?}_~aNPo =q;tcl5CԘ&PʖIfX ?MJ)_gu͓ɫb;tY~Zu8]�7Z W t&e6Z!19M ުB@J٘gGw '(-E*%gO7ƽ+ԪQ@1DZ4vvv)ڶMapf1sFssKXs7 ]md$};bq6VsT{R'1=_Iomm,6YI)d2袧@qB&�qNƁs|ۆ$ PB>˯!+&3; }m伜58oKpg=(dLG;M !3mID}WQ!uH i0[zKFyX6Fe?mHmqPzE3!NIDكEm Ѻ!K:OYܷ%>6.y[G{_|!Q.L(C S_x&a7x#4Yrv>`Or /; ,j{TG̓'7{�RL0 REhʧRFu\.bޭ[woUSkqUp>_޽up~|v5;kyUۨ =B{^׍j޸+>VfQDz5O&]M(Rc C yI_xB)Mp~φyش@7ذ($oS!E'2@ZuAi4"M2"ka8Nq8%y m5E2- 5nѧ ,OvY~4g:6^� xeܮ>]W=>!mcB߀Iy)K�eSݒW"Jiĕ1+Kt>nT7WXOjE=#̀OU�yW4@h豄/v n@[Ƃl�p|2|\ʬ[uF}6E2bqM'˙̬NMZ׳MƊtfbh=GesԿfI1kˉ%+eclRV:atROel?Iv��|CGFs�w�4`xOo־4UlTɡ`G6<yr=}o(ش/?qC6t�*+ n)31~K#(شxG�TYſνocJԬ{O~ǣ.jT/g�î+5lJNo3.?@3"O)<V3lkY,Iٴ AvI'aǷkxx0B`i\*X2dL P+"O�\vy`ärO̤\"r1 y|a풬 "Y6v}t{n!*H~`bQA.Vv[?aAq{ٍy fƒh] b憞W jho3/%C9�� �IDATָsѶYw?^[{5HSn]m avAFEt cۚ42F79)W+uOfvq',�۝^g+:G-za1�-:j.̧ru[4 wK P~|D|o efn>jSe4e[?ukin0MG)zd�8|+9Mc;E^D"ן={`J>V'dh8)u[fQfzvuZWVv҂fze5A8:_oe5f9ͰOL!\z>877??ן͎u ̓o~/kTm�z' ɝ{"լyzu25[=4{D ţG?AVm />,iV4(L$!YeXe T5_!,ɩUJnR YtyfqXe9u9kJ,j^VeՔn| ͏9L4E.[7 Lİ iLCXhM.GI1`l;B�*Ǝ=6?SmZSmZ 8u1~WZ ڡ.Py8�4SUO.8A@.�eI0wŤfO3<P-+9G5WkϋAzpm~'Wfڵ Lx >ި1_ -I|4?r<!o(&UI�$ #/c2!HQY/UӭQԈuVIΗw[ jc:KoEX+{ [l3%Cap&IНDt$҃Wo>Ӄ?on~YE<t܎QӼ"F<42NqI$D23�ַ_^{ݝDY33׊ͶvBޯf}Q릴 `5p{"gI)dWThnD!G_#ޛ]}eoa]ΖKkz K1Sށ`=UqNHj^ۘゔJ3Bѷ'iwWK}9,i1OrOxN<h`ݺu +8< K�<qy@lN�`6ADpyJΐ< U1בeŲ_1Ci-�hǴcKw.�f]ĠgbQd'4)<Ysay.$Mi¿S8eRuݴnjP�U^ØCN]{bQcK(@eB9>6C/DIHKc+7 6aB j6>؉ 5`5}%cހX`vHrHDN8\]ṟaQs)~{.xÓH@x O}.6i@t;Qi WQ/ŨWl9׊_!KqgP=WmyF_B&' 7߼rV}?;G6(&B^]ԔyDWp[p@6jP,Q@(-w>W?tkҶM݈l\B4}}׷߹y|`uo<ISf#Yѭi܃;7l7I45Cɶ=Utۺd| nn]t<mhC턔\K*P,X_{*@6Be"�,Y75gNLng5b'OZNe !]HMǖl]\>?!6Bߐ+ݛ[@VOAD*62btFė_,OI7YT2S S32S̼P6̸39 * EW 5r mm`HM2A vӨ45KԜ{O�X7ז3ÓBP$E o iu8ǀ<(Ʈco|.X챀|I[1#@ڞ/5."f.� V8mx̨o3zeÜOs d*YeIy6[TA& Af~hB Pt,QSf_7^/S'Qdw콟_y`BnĴ`vRR|2@ |^YV Xrci\3@WL S*sta�j1}%= t7XVW`hK`~ZW_NQXВn+jLї| K @QZo�xA*Å>vR.E!3EW pƃ<cֺV]t<I3'i6>I7c|JO~1M1yԦ9! Îdhϱ <4NҰ'BY#Oc taq4> }ب|vG }0N>v:>X4*j\}+BX؍azfVK l 㒲Gy!^OT+sך\'*!]:$' M`" ]]aFiA⻤aĢlehXKw_/MRۥ*.+<;r~"0U5B{ g\zw">5@k4 B0{b2.mC?:ִ|ן=|{oN_x^v^[X<ٻ?_sh� ev#ڰ*l&l߹qٝ+<Mc&eR`b47Y| xI]�RH18k fok-*jV�x"gW.*4$J\{ml|i_/pWm]8g~CʠK tL*4 k6Muk"rS'##H6$A{=ToNd &ѭ|6' yD\!�;֍$:CG!/xMLcXwXbVЯu�NQ#NӮ1-xׂ >P*s)),:UAF6!ݼcb(Ŧ(-y}pR.i3s1>Q\*Kqƭ:G|^7I]Eeb}Ak 8B3 8>γ aDZO]R|,xqW k\\4 Ǣ(ĭҫj,Seyfʧ 6Vևi.gU\\49pRm'SP#=Jm8I< Xg!ۿhSsz4³ X x ~ͥ8V2vDvIDgtF TRX#B߅p587.jy%!kG&[�AT<%uco Wz2>H_y|4\P&'?s4+dQ Q*ߕgW^+~/K{ope3^NWAX7yN`w̅i(׺ʉj\ȕju2|ώoW8ũ썣k _�fqPq<0Q}zC^ ٽ3 #c6շVʓyzQt$z]c!7ΰ둨�TȒfx2]ӇKQu]>^M,7kƉH\;)<I.RNCyEwNrڸ(t! Ɩ,:q+G|Sn";piW.W%�,�a\ bۮ*Q<\\ճ.1�iH7R^D (<L䄴 Q[unH+`'@ԍ P jP ¦m.<>vx!jf#ֵ|*uV~]Owii~~vB((v'y۟e!vI_v6kΎ{gwv)DRSK"'I?,%M8iDӕwMla/Jej4-.ݤ+//M嚫1/QeuBK}d< y"p5Am6MۤI\5aah{U,XT:~i�kf).9k̦D(p-ӂKnȘQN/}G奯}u0';Eg?)R:)#>>_ _; LEL>?{w-Rd%ן|?B~Mn޹'^ʀ7y~|8߮"G13M0҃QN)Xz99-�qZv&^4z>}['0^ؔ*\K«Z0wAk��uɨkGsZY!SI^ϩdiX\imPcK^\k")ӉqLsvb�wM;I%aoow4LV6.*ۮ ]`K>DHBjЄN>bY!pp`2RLֺbQqP9JG[.uu5ydBDp(|穞0(c9d6i8q,wM7UL$8ܤ,b�La\RIjqN5ve6xp+N|nfgvgU}|\-{9It?VP;3dI?'ϙ{F/ .ͭ }9?^t[բLYMk>;UII" bW^'lP"aRƪ'a G:qU[wy)Kmݯ<-Ro?O=){N?ه_޾l0蚥cf/܍[_O/tU-|K:s"<sŗ/˹,bE[8|?c..D-.%y<00uo[w%�}:qg/)iNcͯ75Y̜C۷ƻJպ7:g'}9>H""H �Tt>}#./v9oj1x!0oMUo@d4 �C&�Sa.¢HQ|v(䈢I�PQF^S0mKtK(�ޮiQQ16E,>Q[iEo pZ mS(Z/E[FIpGG|2zjo͢�),u.w771�Q/=)Ŏ._'L 4U&&'O&6i΃vJrNNcH6.vkgm{G88D\J{Ӷ[e*mslr-+\vƮthrDEQu$n Z>c`D̸e/^ %:+.s NN)A_weиFzoXqv`$]Ɓ-TUiyd<sr@'K�r';}ʟ.قn˃C)�VY�K%wƅ 뒐5+l#DqooS2 U?ܛ)ϵ3 >xp?yGOu^�ATUT\gKNz8j-۟ ]_4\ 4Ûʍ#X9 2Ew;[4v#׋ϖ}˼DW)cZ0߱ şMM4ae3Ly|t' 7__2_Y)kWqDf 7O&fu.FYњ> GE7ȓtIr!ecs)'%pM vCDp7Zzޭe'J✇\]U,ӔG!]<| MzQ!-9G{n;QXea<6 B_}P^k*(ۦ|ؤoS(z+[҉j.(uB#<fQ�]STA r/:['58t&dRisah�$&�X!@sID޲tY!ZbIN�Kyfp@ڎ"\\H^]6_TK jgs#I=t%!6=B _׆듊4ugعe:ETz3p=\mכ1h ՍB3ɋ]*bN�Dw;M%|†0 \j~8؟~+j^ֈR�t07!\G@F) n UL7pPC,_~d [wXTU{󋿹kA]QsSџۣ}R/>>y4ZpӡVuՇxw~`' )Ba+'bHƹwYӨQ!mnݼɇwO9-ۆ(f{ZQq S@D'CN&©q>TNXuq-?9xkiWS,Jj65 Q Oq*hk e7dIlr%F1`�/:*ya c8h8q*9k,\4Qw> z]*:^uS5Z;ZYu \/~3SG:6=yUu(,LS%n!E$ jՋ([Fv :CQĮ"x#6}7mrĊb«+S* b|*cI]ȮiFI]z&vq9u:3!Ac(04|Rk{C\yhEݕJr#c`Z: < 'j v cP^jG ~/wW~*]ͯMءqНq5M6E8\m˛U-DBDJU1ʐܷ N] M]''�M4? (@;j@0V5Jy@d+k"p0:wx=y?d#6=-koޯOn-: %xko?}[S]}'{[?3,"B_}z4t)SP2&zL5t6ѽmџ( LhS}>x7]p58Q[ ¤8Ӆ)<"[huyB^I[~Yk&M6\~p)l GB, vӦȵH.GXw�QBKN\*RYC㿟pтPڌq7l>)�R\w\`d Wly\dGjNw'!k7FR;&Os"L]d${*[\ x[{v-mi["OC$JU`D<ڎ<΢d/eQg!p<ם9O��Qkr|f.Jl~< 0]HUnuԬ2>$-o-=LHNK�IȊI浝%uJ�t\+QPIvOWc;L(4!�NU"^[ z3>K�θ@{yz՗g|hDk�4bfMA(EGQYнV۟Rl�dats�! 2*�3.o~ͪq_3}N+_{[!yo[{pbo?2m1sy!wto(N�˳}|gv<r4g?lG?oGbﹽI~;^9|UI(0kBPiz馏=�� �IDAT~[E[4LYkw'[_xvko|믾h z#2;8ȯN#�gg 8gǑiIЕ|rhO`(W/X %O޻ I- NzqYt7 E Gul$pZ]n[!C "ma,e .�Z:_|GmUS{ s\LW $ϸBa2&SKi0: eQ(%igu-TQnK![r89=Cb ?rݙ%vkpwUٸqTC?@^=\Skػw%t 70ډ62�HZ�J=<wy7/aG#G<E!U,չ:ɼIȊYӌV z " K$2½E\JI)COP̸*Z3],!Fd|%Qn h$da\!ܹ̬8cҡ{h-'BROL4'C%J (5}"C?Ad!fWȗ;xxv|~rTUGoz Qv㪛x{nmt?'ˎ!UMdτrW4g'It;ʮ:Mx~=).)zʱi1NըnTq|~;מ;̟ç{kW^_>t]qqjM侮D\DV]r"$QdnMvqb <34`0u�faiCCa\p;Iy)[rC'Ci~O[g(d:٥.6$E"ҌF;3;3 _pÎ ozb=74#QC",XUJ$?.p>$ ]ݒȸDeee%~} N!|4ԍ%ݸ=hG#D|6U�DP8 +{5WO͜UU˥3^=uD]mhL7�kSАXI5>mI/mҷ#WbdO8Di B8y(<L읲muWWvu%>Ul]$Ed+ܞޜvzUT=j8Zn>\TR y<پ @Btz<2ӯ Cxö$]D>>ѻ -&1"ߍ.FkNՈYFL;!jY#.u4aK*ˉLMާ>\gcFk}i:b#�"MMbK|}2/bR5~r'W R¨Ykt´cuwTz}fSo~r<{/<//<'r|^~:S3c5v_}~Y{>\׍oe\ÍFшNW''O]dp4[o?~^K\Uq?6rVbƕqѻNdS_seGq)fO7'LC�nD&ㅲɐ?\U"NuQ,_Uv3oA`<n56d5]?}>~EM]9(Dзݳ=RTs_@y`t&lUjS7EabU\E@ժ;LHYDԺ|l"f$EQװ4%( ߒ;H>QU#214)]fyqd)JʙNS?xݕe.h,x+:7̜(�QQe#,_0ぇ8'5 z'pG[JT:0I͕n"̛1yBiyZX)"->+nwAiW.ix\qǧ )xƔ4E祡:U�r 7L 60ceҾlhuy{�UtzE4, A?'l- 4VnR�l ` j p^@Z00\?{t?uĕ'+v?pܺ++Go33G_#듟g==rpu*֋;o{m)mLl<%?{7?y~~=?~a6?-  -7/eO!eo"m"ڟ|隬WМ4" n7bfQΰZN( 6S787Ӧ|V&CA<I�%!(Ƨ)|Zrm=FDVFr v�@܌\ QcE^P0+j3oJXDBۙ^[}s jyX4sQ^YWW\r�O  #̂ qճD@ar}Ӧ]�4PN0ᝓ/uJe2pt+8C5O;V#DKLaQ$Ctin6'<H ;3jm@bB)窮1`"r}l ?hذqټq~"Uig^141&(jh=Nh7K/:c7" 4Ґ|nRq%"W;L1F wZAoNgsϹ^-7F9Juqb\^Lu̵cy>6Q٦(ck4"rakx ZgP v\wfo~n GB}N??չ};|fe}oJW^Q߿aU/v|.ί./O.Z]ڈo>}jfo筗v`9B˸L?\.T!u?{&//~٧g~$裍gE-^76_=0R[�o'ڍ*<8x$vۜ!U*Я:Ll9\И N7;dT\nE%B ~ J"/~cB<pIf'�38KOD�,_Ʀ+(}eMQ9z{d k"GT {cg@Ffm|6O g<KX'd 1tA6}7bN \ :S<l$`9? k�9>(ur�^9a|ߪ+=d(\gcQG �L޾>C FYs,4V'�@ggFe~k:Y٧3~2QT(8*x%=g�1>_+MoEf<4~J/7CUL1kƐ�ۘ_d|YJ֙.f8 @:ZjyQGgԇ0;?T 1 �3Sw 29| 7~SIT1oF=|!]'pc9M|^=jV盼u(re>֟7< hr#Lr3H~'~O_G{W�l4bM!bq/Zby�gy}@V_OQ~x/]47E^^8NXa,qP�EW/&j Ԇϯ LQ*TAUyQ4DЏTZ;S? Cg @^=�XXVykKYj+J*D35I<D^0D‘0sVx9: `r"k{ <�ˆ�ѵa gS<ouxE}xL�jFle?J^g"x&E-ל:(O{t(VK4y6!i˚Pzzv =}{&bNe}md Gnh70^XUr zO6<ԙ�k2[\5V)lai*w |"p7h@5"!Ü2錣ԩ񫯓(M7Y)aUuO>tv@J[Aq�Y?̲4}T9~<b*WK�/?wyYkѽ"~>PΒ2Qo>]0\>0�j_U֧e@~-?��/>'H壬J7Zga!y0=r^L6ϟ4h7EbD{x\DX :4k0 |YC!J?اްO mbA2h&=B,lXйҠ{{Lh:dc6HL q8Va8~QS#�KD>r4]`z>_0r}x]̜OBFox1D0daE#)4=49KÊRzT�'c6$^]a&Vk'UoEQ;xBE9 NÈPCEZ~,"[YDޓ~8tkȭ*4m"7TtsDH͐ @oͯ)JM9h1jL=3jD=%Nj?ؿ{gaZt?\xVz\3Fgu}|tz58<͓4$dQM_][{nrb0)n|0lM�r&d/ݽ}WS<ӅVyrRt4c?~/eY\l�x^*2.DޗWJB!8i4DrQu϶в~a>JH MsL/gԓV2FӅO4} $*Z{RPX2!&#]B�r0"рTAy �i3d Cn=GppM8$.Ea��#~emV1~cRq{_HgaxEsD<%Azq*D@o_rOKW^�3UdVКU˫hAz٠e;exzXmYq<FLL]k;Ω* y8UDŽO�j߶5j<Yꎫ& -/+[~&t@/"V}?d@͕3o]-+ ;RkctO5T|l%u!.Gg4*!<-PÞgOYq'U4eZPfpkvW|_"q丼\~g}o}(Q聆cycoOEU*/[h'h66<>q`]2rzm?ֿ;;vwwvko'ڠ3=cRbS9Ͽۋ|糟7Jju6z._m;uWp+^4uyt͓ǍX�+DRnugg E[ڕ|nFtdU^HvZ-$ lٴEl븍;Y Vm8TWBjl#yEg8׆t%"keҍyWM,RU$"k6= tpb SE59C2�!H8H i,X)k-XqZ4�HnVUv%TQh<S:H075wN �֔-DJTׇA3E}Q¼m-R $-$$�n=|3S jmOW-iM�S" i~u;SV jhn=sIIsl85wʙk>LcJn'yiQMAhĭh,']aLg*õsWWɒݧk^nл"2NmeЩ*ұcnoF]AK;Ѝ=t[ԡyJ(uݾ7F&I2T p54H;ݝGnuk#J9.7/?|GOEfiD%|SP|mR}rg_of 㫷w+nUzD'շ"o9%cL1epОQ@ӷ-K3{G'k�8;y$ϻjyY䍶7Z։WJ�OoO^�, RɵqNi.:/mQt{OuaeW�[jjLfÏ:>X  dm+q%Qo| Nّ> [ m3%6| �Ӓ "}  5{U]#8UdlSST,te#ẁL90 i+-+G5]뼯E0l;bLD z ~ChQEuf?ݛΕN]9XO--|Yo=5*庥T@#D~F}ղ*V8k~̯Z^1wZSWM.iFo 4B)!B۩Va WVAWjݶGF˱Gd@U[9w붫[[&YˤϽX̞xlj,e_"Uت;nƶ٪;> OVݱUm8_͗~,Ϫ-/[udz@]Vwgeurs*)Ё eVK/ߔK/7x'[Qj#LwS-MԴ45ԍ~^iaDtk9jj ]8;8W_{xd^]ӆ~~R/UÐp4@L<#Tegm}G_$(]\Aɟ|cV_tv?Bi,mSA�,׮�(Ȁ\եpN]lcjbɴsqa+5toqpȔ=U|ViKtMoO#D텔Bwݪ3n7- ^٘gNmHJ}9vlMݝ "uDlfұX:~յ,m;2nN:R#JQ"YER2C;xu`v\ ni5Fϝ렫k�N{}rbUU(^:|+F'4>G( � m#u+G2 Hj?VvV5=! �<OFhf8e# �ӻnC@U[غ<qEguut51k5V8Ū܆yi[nhumj)@Mܪ d6 CRUH9}GDS+J+R_0OD� zq_Cʙ>Q%ХϽ 2WND' gyb(wY6l7k=&.^Cٱ0'U'g> O\}]N_Y`]s9sujb1:_RkH�8iԈU6&D ӕF@k9:EkqtãW[댞\ΪX?k ћFp׻zp5V\<^6|VO..zOXxGO:r6. j}~ٮ7E/6uQJwD4 =9^t*k +շպ]~->eҟumPE�� �IDATҤ{nz["S~$ֽ;<Y>./ȋF|A0[AK �|~[Wf#r.e/V�(Zn CD @#D 6 G^:P--iZi &qBj,H֑Иp|`#B@9L 6ضFkEiN�F[ie=/e_եU$agU#IFKmkX�4$?avPy$5h_ڇSƎ \FY1m1}]zHezvvYZn0-'#xzߦb(Tֶm[$vwuDS Xn�@qXXA?Akd}@pB)!|!|՗!^ !d0b3f�;Sۛce֯,c��J:j=\LD4o]<(GhƙsgNIz*|V5BYi޶uQ3̒'hRk6w)P=Oޛf�60ió>D�@G[r=>mM*l~}7.(�thP ~±Qʼ|[?w|Wgح.Vgٓiշ畮k7:vAU;ņ-Ү4}u;LiEʯ&}M#> Xo޿&qd[V_>y(�}Ip˖Q cRW6x)_M>}盼|&CA #>a+zJ'hF4YvZـv5ɩ@EJÑ[ Vw'W].J�\/{>C.[8QU5rQr}NSU]dflo+ՄI7=v9Xmv0 .R|�xmMES|5ٓ=B݈QƆl#|6&y;ug]+>,.L 3l$-t!!YQ^-%I 1X cL,@f^5gW�(!z9@Q6ad0*@`R3%u43\>XDיHeҚ'fQtB ZTCYPԇFPSuLqfP'NDc3 + d�94Ķ@Bn ,E�p�a8Ev/d#=NS~4qb #�Dۄ01ҹ1iD|3m99j+V:|ֹ,\Dz,#/3}Txxq^}~]} |Hg? E,f[f<�qN,ў <EY&lzpU?p@g?L#{zfJ&Rèɠ)B�nCaB hui,31)'01>fI֯�A̺zϴQK"Ǫg<AI:;|0!J<@Ɵu<[:BE>=OŖMd4" qwdЫ mbJdu K7YH$= |�b6zh!,oDm^�SJ'=_ C& 94Bc?=[~ !j� 'j;ρUaױ<y%8VB^YzQ>Zd.Rw@)9WPaas§al-m&(5!Ba&"yU#Ft2N;rQNձIt ,78@0F3$8!(dD&l{5RsrDIWA H YM)Z.tTpX�6=U#|4„NObMc/ Q Lq�lw~GX (MmjTQTj\\]R++}Ԯ;`16}~y'�P�\h]mi4 z> w#ilj/�Ⲑa\By/: ?4wãp�TWEQ7;fZIR}YV6!2hf!Q!maE)ȷse 9b1+’ Ui(V[9'?Gx ? jڒah,J"L_�g"ߴ`xEm/$6I Tfg4'b}X>'`}y`[isx`+( : d*g_ !jUL;)~q P yR$5?Y׫4k{f�"bq4K�YlP Q8'=6,^Af"}az�(x:BtFP=I3D:C~1'f0<e}�3f׸9$3۹~�l4R Ԇ =BjFF\ȭ$7Nޱw|KB0ˆ ~*ώHQ,ayvxnXh.[R֫\ @`E]XD85т~{/�,/?a\5� TMpxt>_0es,/HgH: ?N{�R�Y|p„qx;NDet\~x~^+<Qjħy2IQ2D |z!3�r% LL1AQ59@"/ǦճMRil[z&v?4KU|�1Dl7I2. ݤ�J_61X8XU 5Hk| �F(uz�N+CcF刓pgDo5?:W@-@N~Ւe:|_ev _>^\1:ҝkͅT\Μ ne=[>@5 kBJ@# m8qZGD**8U4f:9ZB] 6EZsV*-G_Uv$6jVRAԦ245N 7qm+R2{`;ˍflۏ7\k;71: 6! VSٶAUp9w]l<;Lk�plgGn^eI`3{Lq#}*GRCo}ioK{�"�3ty[ԓMk^1J2|% c'Re<w׾9)}>}D%"ˢĺDONw6٬6eqڜ?jU^ӧٓG?}z˧O/ץŊ_&Rr-X[{K?.VT?,-՟%`�X߉B۴]l;~uY}�POybzSA<vr#|s#*ۡt4yҍMVΜIjE*1@|tN7,CGXꈢ�%NW�Z7Cx<E,4 ܺnQ_+}10 bHQT' j}�i hCKH7A`G.!ld &tn[64D�0 6eW5nG/(^m ò-�SZsrc;eXcYܯTl[ lF8yk歁m8֩kƤAo3:yCn9:VCiˆk2HtT5~19384懤N� A#&2$?Znr\ύςsӿ\R[V uۼ4&pFxnk7f]kW P [ߴw##P -Pmܻ-_zgp-Ĝn>e3úU*s?H�s8Qo=o}r/qn._jm}X�I�I'K;.oЗ=cTFNQrSy~Uj\G09M$+.+]{+kx/]/'/ޥӔ%A_EM-wzg/H!J�@aCM�Lo|uKzak!~cy_ ^8u2a;uA5;kħ`j1u"iswȨ QsRoW�/igⵯתK`w|Ӽw\x{@ C?CTUثg ֽn}#׷ŧ#A,0[nV =>}QF Q|ݽ@EfUNݸAUK*89˳0cvPۊe T,[I=<!I*FsO.Q @zphA6$'јqOI $Z͍[~]D(kI"k鸓8Ƙiэ�/L/�$109�._dEì�( *||)d >i <6 l#l=g Bcjs쯑JU2"yx+&dUE%)B\(", (4 Kn+142 /�! ?(6 =A/)hΊ| G/'O~G~p~rf4S6G3t<|@z]Cb.P؏!BR7ŪߟK�sf ?y_%<V(6�iO$j*}Z/a"I ÂSz%R}$b8τ7ܤLLx!=W^PP>P|,> hlY|Fs§XyCPM � | GN72؞_Xn@>RՍe+3~Ź<^+0R0+ I/�H sP0D}s�?+59@-:/Bͣ`)ٯBװ= (θ`w�CAOiN(4c Ej.&ؘW Dx o#\9* ѬЫ MRffS(#G)HJ( v^$a:R$)8=T4n'уwͯ �NxX$sX8'?|"NR�.3|7  ՈUt ^0no1q6 ^q�eCvx'Qx >ua~"|0FGLCl?S(8CH|űhvËUϨzC<aϰFX`Lxb5Un'V/X@(L B%=rMpqpv =&Lp�AQZzګ6,o+hjD9qah5pIC4VBWFr6nN4+:% 0&vo>F:#D߃[e[\V \kCϠA@BjI35ya$@cº, <^]U )ދAkG{W^SUeo"-}ڮEivیvXT�AD=ir{ ]2}{} Ζ-ENG@*hPn,G41+ƭ5Pm ;VeuG*]^:sR.ULo1zϏTwMM~*q �?H[#Z.]ƙjq:/`hݺ;~ -wٯ�^V .ƼtK:x}7ו*suRh/MN]fIrw<(7=7>xsV܉]�?<: = )�خz{6 Jxvy0H0Jd_oUؑ͘L+Js*GPM=UD^S(*f-!.Zo];m'L}znق Q/pwH[Y;yy @g,>oSY&,@r�L,oəTyZpsBIÖVivϤFU(!`48SIMeח|ImO ^.EQؖ'2sGyw~S *:,x;S�-bvX3�5bvX� BncnT�Z§A7Y5RkﴮF]:h4uι>Isc!B"dy T1bmy=F^J?!x0Oi:.3f}YB&�L{xQ,^O_<*r} oGz] ._W;;K?BU;mښQwԺVՍ2oL)*2”1#JQ5:%Nfמi*#/nb aBtrS9jMІ֭b!sgLt])r fX̣`Yߕ0jcX!P2~fi7|ru)D@&B,`[Z楻a [�nڪmgp'w=:Oz>^xtF; qklT vPKiɭ]wM >ɻ9W}.[p>�1^hN49MŤ+A62xon= i$ :BS@֛UY |YjSkǑMl*2emp�x@:f<AXEcy(J>YZj%Ccɫۖ]c.%^t:'y+:nxDM>2rZ8q_ASDU(: !q!Z?@U3Ui*J9ھ[ <wlchtXɎTvc^xHmNj U[sc/@X+7-6^$ Mnj~T#NUaP,+OXi5S&Y-S9$^�TXf[ ֚0u($76MmVµv$yÃhTNto{K <EM9~@&,>\st�tmh\JS/hxEm^{Wrp[~凯qxv0۹{5h%wnN|?n}yyt[a(jL5-t#~%mG|mjuZjz@(yB%{n߻4rJN=%d[n}}ި[v5b%|&9pvU-?)4Sb1[ٖ.*:+bU;H7S?1ِ? "+6]."OҌ~y.]wSj4viNm@]u}uoζim+u`:N8ye67u\,Q$+ţx{P҂,6/YQD, R<"w!lj3j-B sM<#REQ�&bf L~k Sǎ;|G)4!<zұO63-9aJVM=8S92y/*mۮn hEtcƻSHuXώs)1e@5nx e. nF: Eq0/OlݼѻgWH&R'kVs&>{KpU[{&qZt HLTR4W y�� �IDAT1؎χp8,&A a3Q,8~AdPȬ|)3{Ke{(RYf5w%G,;!ϒlB'dMb�\2X;Ѕ`)̃}rvPc_%E$<xW_?zZTJvj!<�x,&7r):9&/8[6w.l=")`lxtvW_E쭕~,ʫҥ>a%+~ f ;jz9HJ_Z$'?}4$"ئj@VVA)xSa~Ӱ[=E&&o](t4?9)[cF>:B wwvH \_z/Q*P('N~_(j:6�-O_͘v*~ڡE_�4?k{9{s,K>2}Tqox6M0iWZ5p1>Q( %Ҳ$er5Y s{'?:8t-2`wg68>O�S l [.=EmƋRw+Gs3 J>/~nD5F5HbT@sO0$1Ѧ#| B>+?"3ҹ4!.ϳ,g-ʂl);&<,~M$+H$aa&+\i�@Q@kуg-t8Xq9e$(ՆNH* ϖ~}^3D5�5q pYP~iqʒLrx>IʂtDz&C?�'|ʹCƾN?y_??oˢL]y3.A<:}Y>8fMY_\rQY}rCگ>" �Uu0)b.b)8eUVe>}qM5wzb�EM+s�kl/3.mG�r`{OR97Zgk]V,m(c4dA%HDȝi� ޳(08Q9ʳ?o>ίl0YOB_þ\N�;^ �6o?\c:Eo]Ԟ%MD'zUgӒW"Dl2}5(i%\?ӁB-u9!"ğ)>ـcIЉ`͉ B_]bQ(*J8(L:;OT>ª(ETGi4w̧F$>`&"JYY(޻ɻh[Az6K/d#r^S�hSpbTA /4ǫFc6 IJP8A%DĂS4L4LW7Ɍ@JHLL�j?Ak HloDJsDN^(Y zC[4b{%DSK:@qe\lJW)i^+x,\꓃@!kLIEq. pIO8_�\L6H"3bNIWgI~?i|WgEfZ%HsB14WY::k/NʬQgc}g?L?vjo"4>tyk�Q>f+7J w}q'[ͻ;;6 P&bdG=Ek.>)5:O"1}B9[fjgܢ;$Aߣ5Mii\0$I2q̈BmsF[-u/mVn�"u1)xa$ 4SG!4P/]Cno:OC 9ħ^Ms4mWeAaGd` E,N(1dk[^' 'S _fSg|D()ptOG!6ZZKkLܱҮa dP<RDeIq*&Et!o։P''�N�N"əOXbitn�)%X�ND3DoJ2*w(nNk�"X1:w7\oWOdF�d;D {g89FOwD?(rG= ~G;NYB9a'Tq�C鬝\HĮUQlII àQLJ9!HE荊b[zdp#b.Ve+wggMvz6f_dh(ylB8V"YZ4<KuO=<;͸>-�9;'L|?hs;ٻ9Td1lK,6<iwSzU(]>u__]|I2KE^OgwzFLQjG0٤޸ٸ}x+?1yW>aE:(�yj/6h%Һme8�z rg�}%)_ZeY(FXeqa+u)ꔛd ~ �c)eMByuK#�(Ԡ4H5�Q2. d.Q(<J]'QuP^Eq@hw'kq?(\#XaҌOd-6z ª2\X;9~@:ɉ@o*:/`$_?�&CArX IyHMnx6b$Aw5ke:u&2KR,_?wD0J0o!)*'~'2"K�`<:;żg4NT*ZxP`CǣjG#I@㬪S<%=͇~E|.Dhn'nI[xDIgs`fiNvbNÇR~{ۣjEt[MsWiŏzvYο~/<:==|<OI8O4e"M"q&Ezb'�OYN(gݔ^<,Htgvӗ/O+7>n t&0(&nl;_vыxY}!e$HDn{ _VJA"Um:--ezIAPZF,s!#Jfl0a 8$q4iqD#* B:B2D ÌOX`U6HI<eM�Q\^WFi쓟ThؽO QrcpwcE&X"ƢpԲ%|:Ybe)Ag9Ov>{|#Zb=V}Q/#Oq~eCeS-�}H7x{^ mE5!@D%Vc}a`HJ<[swqc;PH~ �"JQL|ۿq|?Ɗ݇zPtI =t2!Mt(5dg!(Dz*P)Cv⟌L8mK.,SX epZ/TEL5ʅfC|Bpe*�[Jzb3<jE2 ?Y?B=e+m%ݝ Ƿ^uAz'` o7_?_^>{z氣+Md2+"NҪ(b)$)s{Bg�glD?oR;L,5^<X&:G�'/Uۧ*/ɥIchvTWUՇQuKyxEg1Vo%"I$Ž' Tbh00=KEaѽzPX>16>5^DiD=i, qT$F6[ÚEԎs˷Tl.DN=2;cN#rG *�U(K E.PXx&]|)QBA2-|!9o 2O;g<2Nw? gYJc.E{RD.(56z+jQRJ`4gӀI&Va詞5\ẋ 3@1M)ǁ $Q,xXR(?"Ki?G Ip�bd7ITV�NϳOuঌ1uW)pl~w 2r';QNNL8 QG*D(s'}_Flx{{4~dw;X;3d"E cYD|cȓ)*s,�cHs4'LΆÝ6t{Ť{3Y$N-WyZ\k߾mF=x @ܾzz?_~OOt)V+߉WL8F84X+�xϽ \ gu2+,OKD@“dHٓ˕hbe|žj<);Tݒ6 �'�QMg6.Ҙثӓ7]%'.fG,୔{z$QN%J �_ _T,x,<Z(�%BO`R y"Tz.Q� ndQ̣2*l3)`(EAp4R ?˼<]9&.i. %{GQ%7$:gEj"QzMĢpy~('�ZS© EK"U}"r+$J #(P H@um TI|DbQ� /Y*ȝApD6 *RTSdxQɪOI~~JeWWi@ ,3U 0 J@zT3<I'" GidVk%|Ȓ y G<A,u@Q6쌍ȶ1(G[}A6 Hs, mBURg"ĚD;/o>˧W�k=OU,:7v_ _/y/r.<qo(}U8dWo0 Buudtj=X?x "=?NN6O$ޝfY1<θjbHNWia/3W|?Dw>gc_+|E BXcxN|hؤd̍Z VSOw=IjRQVsƯe4)V2\+;ZO4'D}>))4ұt(*KMRD"g˒$O<f92x\˿ʟLn~`YF8͠77]ξss>{T``spWFc2}"faE,%> (3=aD+%gOu/"L(<q N֣}=AeGip Y0JnsC륥 " #2 8i}/`% UQkIA[�N;");H)A"I m,L˲1OwPV x�LQ7.?w4 E§w%F �Ta^H֟ȞJ$2ip(TP# N@B �Vr}eSSymsg|H9-_{ѫX׽VЙ@{}gӓlruon:{_tDv[.z~\L^Hfov#v|v* Z+bQ$3E*_U*OU*gO,3#οzD?'tSmraza\FV6nHg^n?6(cy۩:;[㔟FݵzqU4ԃ#>JoBEeJM ʉ6H[ U*HrJ$8% ^ z ,a9srŽY�"R(Kq;J,Cf9nohzCmwIBu%�P.y(Wq2q剌>5-Byx8ZZR�KGyu˔#:*LVa'pu'?ABߒKͥhd}"}jGIϾBd,dؑwqʺXZ0VcQMևԒW,-=K )~0 X^$DPoV3FQ:(H#=;?2=lK41jQKӊj}]3kCݦSR^[>-XBabt1vEwF;iD~;nb L�ӏr<XTE C\>O大_@J[KvNMB;c2H"�BK5II;=[ӵqNݽo5V/miEuZIj̅uov7NGOnJh@QmF*MWIv8v6jq,/N2vr~TϞEy7O7OJ.UҮQ,$VEQsy~lbV5N~q:}ϧQ'*i e 95 y tg*2}X'y$>4A#ďiH39H$?]ͮ$3Z 1yeKq~4aWrI;*PrטtiSѨ/556~z'z@h<C2a_P@WL^ zQu@BٕɠĢXIB=l![Pp"у>y1))+/Mtߺ0f QJB ~ӮғR!IpKA6+ D`x<+J& 1*2&*GKw#=r?uMDRe.0)CNCO&MYcWDpk r-N~Ds 74Tv6<C� "VoN/sǟ3Lbl0ӣ_-�$4z4s#_Q#"TE)ɤLڀElLhLc~׏u/?ff7nJtwS}or? #&<[)c+T`vv.v3|E,󕵽f˜3qB*>9;yHc h6}M{?:qjhHzP8:�nw <[BEWbQߩҿ7צin&N! rP՞I/ ͭǦvbEp#J:!x'ѹw: JwS?:o4*e~M H٤I[AN�d6In{A͞/{1\PĢe.i~dK˼(-GyQd .t?O, Glj(I`&Nč21Ic/) NUd.Z6cOvÐĴȞ@E\&x ;w&NfQ1S̟t:li}>']k+i"Y"k(!ﰁ6徕XBR,YǨ�@$t`lN}ԺY<]Uhղ}8 qV@JQ>1&#�\+3[\ɶRe<j([L$Ю�08rm_~{\UunoÛ׿ߵVy~:L�L)()SB#k{k{,(6N6Τ0Hc$G[a<)CO)�՝4&N֯ؤ+9�0L䃺 >U&`E"$Ҡj~CiCD*�U kw,j#/Ⱦv�� �IDATqNKǼd*h^k{?�XIzr*/"3$)qS/Lv&7;;jL�h'<I:qKD|r'h=jq܈C%8%cGQݣ!5j Q!A@`SQ f<ȶٕp{*oՠC=AT$(aBC_hmu`D2ڲmŊ3Q0hps6T9{@MBM6/;Z&RoBQU�^-);feX;|N+wV@-D)ٲ&EN_TvנmkpE Q [&>*|Th�@MUB;0-ҋ63�=]r<N9WKxmvM*ir|2]]2]i~|[#tWs�0v]q]]ak~myrk8_:bQE 7.GBX�VM; �vQ6,-98>UHjjwF+Ͷ�Mc%|K!naW_^sXܮo0-uw[-dvh@pX56u}2 OC-ě >*$z�ڢ|!;�104SSC#*pvѰ!a.uM�Zպ!-?<4 qG #Ւ6-m~{d˹R>3{?kXBP8RSB|5@iZUE!jP 24I;:<G0P+fGJ[%!B`sBRaU?WEN|qHۑh֕nI@5flFoUc}z�GI6BڴG<м~j�%+_yl>47�|Tx߷'.mj3êmv�X�ᯗ mmRmR� QFh{4heyZ�P۹fy�ׯ~}8_ۗ?շݮik F7�*J�F�W*\Ս%|kUZckU֪pk�M�+h 5W�w{;BCrzd>ZpB8!3lP3iv%?roBǣ;\P{xI(e5o;q;)mgK>5ZV{@( vJUCf�vd@[A.ʸwjQ)x&naNo.]|rl_ؿA=%q)F;KEsXT VsYmhRRSaOzDG0!pS̷zXrTnU=5f6U?aZ.-RyRp#U`TGM:˪kT]*\ Z7)�~ ` (b%\pB@墅+ )$oѶo,KE76-6tֽp*QUU@x<H|(mdM8d]i~o &7tUhKEWT�Q(z~¼`(oyꏿ!| j{J궱ow[k5z]e͜,L>7,%~p4C0X)pr?s</]�KVZ9'bxh �{a~`V $KHe_} `n5385|݃$]};TZwPB�Nsr �u?B[Ϙ(Cê' c=Ux'r%\{ M G0[(<!�cZEqXKG2>ǜ= BDklbvކ�IJxc!SM@Uw�-SV*?G-Xh_oeCs,*/&Ģ^AWKjƲ 'IU =]:e;tv^˻,\o[=A^ow[1-"Q1rLKzB2A#dڴ�\R5h%\\5/Ŗ-=>zjB\tJ[�׍zwP&Limw]w *O [)Z3Gk^w߾!cؚl߾ݫۿZkb"qN-ǜ5e6pwX}8$62+ڡ4Q$,po+֊&P(kFyZb̰\"_% 0ڎD 3$]ܷ'ZwK-.yn-�`Y�&K�mQV73>dN! P CYT.J6UVlo9aUlC=|VQ-%#sV##@Z>SKD@ ]/ĢVFg{CdQZxR(_ZR?*p@)|D xR~CsZ6H(8;`UPB՝ ǀIDpNm ~/t8 r(+Lrcr >:g�]kZ9�(`,ݭݵ3JP-2FEDԷ�D". ğv#(ZsG@b�XsۀX*ӉjLEZ�ej9vd7oPl߾9#s,8~>Rt;tќQJh"O��4oUk_Uֿ`\獭;4^bֲ$-P�xV̶*T W!EDDJ�WH]f=�WvKXԋSHX,s |y U�Q8N"uP:@rL!k .$.HB(}Q䌀1S }j\`%h̫rF@Etȴ OGD궶K[J-m@QѣH"|8|GWCD�""| hUA@E_<rLW9|6-T߬8o,٪%(\_=2,T.%咇uSm Sj_`_4`& ФMԥ�s{ЭB7˯*#pڝ0i'8 h>ľ#!i+a]kf.׿;m}�7OGZ]- aEZJqw@4`Ull;!eiv##1mfO?~T@AlZRic�TUU:K$TKZT�rb/ Z5b@ΰ5h�Z- Q(�K^}^~<(( M iʏ&a@8*/ XT jĢ>l#'7ǧ!<bQ%P(XV%jq;=A B;ogCbTE_O)Hv"`U *B*�owxF@BjQ]\T�7HEU^d �^k@ %V�| 0 =)knݹa_*eF r <ZPtj@KZ*~҃D-=VzK|`Q&R׭E~.{lqvWBB(cu޳R Rv) Ѧ(9~!s~(ʪpш| /Қ.s�_‹ۀOD8wUm]Yq nnkm+rVW+p7|x(,%v*W;G+.ҧ\fS9@�U Y?<Em7iEGQ]|yuęZވG΋>E3WG?%\`8Ӷ}6S1:^1]axfU¿!^u[ۿ;h7 :EPt� Kvƥj r^p_Xz-֘%Ӎ,g4WJ@L5�DdMi監KQ o]׍_c_�hSf:X(FDP_TW53�5A ŜH#PެrIȄ U6{OfJD_R?6p|ΒNcؚ] (7X]%J lݐ {δi NYWj$z:^C9md.[#M?Pae~ WӺ�`E[>|11oZ9&Ct\Q(>�k`7/(i 9V!`?GD8tZ_춶�g2gXT=:j/MA#mm(Y*�B%8k:&�,}lI**speإ 0o}wO,?}RT[־PzgwHQnmvuʝ/w&d% ޒHR|}k6^֊PJoovvk (�씯 m hTJof,Xvt ԛ҄LB+{VߖL�cB] 5(m^m~8TőƸ*@cS|#+qA2{vfC6hMmɓo+~`;|#9ş6qngqBJ9KѢʝc^y�MQHJEBcrvEe;�қ~`/!ڔyܧC�<e_ D-Fr� sc=s|ı@O6nKx@L_AT}rgGrU8S3,d%;/|#cCY*f4A|*p)|z T �Ke<R:'r [E{&kBͫyٖenX[״mlv?/ , 1VV\1W|6KY`ًZw v vtg^ | ڋõbJu%|J=Dm7{/�vC&W �Wd=1)Rtmoo_ݯSc K{>ehf?!36"U4VzUBdK4=%Ux`޳Ɣ%G)y}KPs߶ yTGv . (T<. `ޕ&oO\]l^=jÔwjh, YAU >"O%wFXȅ2m>o{z̢˔92@X<Ģ?y`:ˌ(`+QzfQ9 1EYE(ETZCjS@ևOdlQl~K ^8)̑�?cK�"Fyρ[IpS4�wv@ɮ@brP<-}_zHi۲AזmAfg4k-KZBbW+ *ݬ`J0h&�l&w[:PF�kWk7 Zl>h(6s V,Հ[ɒpkۈYͳh?!-7("98Wߪq8z*ʃbtX-e:T} p]Pu�0��زkaL?)S~yM I) JKҜd{a[z|YtiPKK, Qw-~,(] Z_)Ȏ!FYʔ*Z} KGX(BSH='s; *Ǝ@K�Jy^u�(zmeyμiin7rX*$T.l)l`X"R8�(:,6ץR;-h؎R}mٔu PNtxz/;9^r>$͏`kED V~d~gx1@=T1z?J%9W)- 'akԷjIB-m#hBjv&-LThFբww"d]Gal!z7@-q,|^*Fu*2-51&%G78x~k�%+;Bߋ ?-%�2y} IGQ)~( >GWEycc >oJQX+eQ^!P*'ȔQN(48=,e/H-jy 1z'LT0iA20ĥjLQ.z3~Evk:UL~qY7J:xP  aQ҆>C 8<Ɂ⍥qhm(v�-BӠm}ny6uitPm�Hēz{ yA\v(ƾVp(&9yS My1-㶱{u]*B :ak[|h Mi?>`=,`UX?*Dō5fsk%9>O+4˻,7':*0\q(6 G�v+%%9)A7l�=6 12H*a})"d>)N;J"@ Gu\sDvlG Dz hꩼ1xwLRFy)"�`yd2V2}7z,5DO mm*~[<y#H*FI \Q#hs+fW0FQvC3Џ&8F2@7h&6bei^yNxG  p|lit�d 97\MUIkh=u:99ni<G{ة.eSʦL 2EZi-{ϩU9Ჵ;'!J'{J�iUJabI^+dov>¨ڤ'C,Ѷ2&墵VgeI,YJ[w__o#\My!Sq:) @Ki97QE:uE)kvC2Iv*2zky"Pz^zX1xZaf|xO)_=y |>21ȸ_||s䉘\֓ t>tq_=\jQ8XȻx& 4�P:P(�ĭ$`^&̦ o;޽s1cR)o"a%fT %qvYm @&EQkmɲ#d((4NO*GnL߽̼E۵]/=M1D2ppXrfW %(s1�\8ȦÔi~>ʖSZX7c!nЊ\GۍHC뉖nL2UҲd?ѿW ğBQK$HH㜥"yM^~J",:jV!�OG+4خSx EqBO:UOwԔz}q4lM\�TcZ'�uJٱ%xR+i&�@)cC:΄SJA5h84lB7] f_<eww[�zO�h}ϙ) �)[Uf7P 9˩p٬[;J�@i I/é4tßYu{k`P3÷7�� �IDAT"S܃k6~i#T#78.4}]S8 ηBƄx~�QxU0S.b-"=> IFg�H>�P0�8Ŭ2yc1 R3mmTg'2HXʅH\rBQyO*Bgb&HK<O�ym60!y$E%yzhLDj͢T>躞L7Nn6"V Eƭ^OK8R�ݔw"xS'Bٍ*J#,p7~Oo;28lw.Ks.qzV1ʮbW&*~%aī ct2r޴bp)).!ZilJ!K$p�f�ȍy05h @4a@wX7anԓEg`>f0"M/^bV�H%YTՋA4ӣ3*FgÄTe,I"�_evL.bwfC�}7uR8^&|t#x<Ll1.sR ;$>38b�߹H%[2y �8�Lc?\Rh̾,sH4_E0fS=g0<[~-C0qok3æ3 t^mJGXEDaIT""+>LșI2,v's1w}StpE5:**I2F?)L+~f %%1>BQG["~ ̥BAns (h,&'[5l?4yѣ(4lO/G>,PqPzf/m'K:)Z?Pٓ{|᪣x?r_~ߨu L$IGӈa[O-a2Ƴ.3l0#�@~~Z qd ['tr/ӴJ q'�,d,/y+o~2Q@9^AgGֳ ik: mm]Fd1Mk�h-C{Ɖ�3u�[1S]ktnjW�]Qv)fjBc(?ћY+)7� !jEޚT.JeR>,R)Wug ~Rds}xW!DKH; Y7/-~u%NVky۾- w)dͤ^4#^( 85mӠ ("e~U8rfkl;S8A;\�xGQL@ x]Gg^�RMY8%<+'q:B(oQ-חs2?Q<^1Pjj}ϣi9l9^Q_jWH;pt @V|PwJ5<f{RNu㌨Az0<uqt'K|7׻"o"Ё~y*aIb qOt?fsl/'mWAC:v=_cQ�n:jy8Eg:h#Ӹ6+q�ge $/ڸ{kXk.3ڃvGo,'g3thaZa<(vSLjVô_ *7w LIa?qM\:v�8+1u2t|L<9w;xC,_:)jdy�RJG8N,))DQUJWi?_<ڳeR.RB<� dȲ'ݶYzc?5ymmU^3�x  BQ%e*QZz]᥎&PQ0q(LyDg¬zgjGZJES/9ݻң( BY x~Lkhn936>=Ç@œfF r5 H&ȉRJ�B=)WHJsT.xZ;)K~<r*Hii<n_vV#RtmS^`b:wu?Q�5Y!grxlMPkZNiɳO]PCT1Os(ʷ©X'ɷP~KߝT8d7[&!,kq٠*`2OT/J\f�gIET E)PkM*&ۓc dQ [so�3d@oMYS0�ה5uk�- ukGV )�wQ�\e֢.?xټ`f FP̃'�Z<_̚70_HS^�D4:C0^CBc:-!=ό8mk/(QiO F߮w�>hۻn}@|Gʯ`.yx3 hMP7қ|̵~Pu�n;lȴs 3~M='<glfU?>Q=59(b# |)O(d '"&ѓy}J;6Q|J]wtd|&[dF'fǖD,`ٰЇ!܇̳S'б$Pg'_P?PQzZR �kQBh̖�`6WPf3ɂS⩂H!K33rN&CBk4 e\"3Ӣ8Aٮ0h5]DcPw6Jlows#05D01n>Krs[qaE3n�06F}]זLseno+�@.r\4sP̕AdB0K1|3u(d(#�ԻIȫջ_pd'ݸ\\Ps",fB?xiYxI :en7 g0֢]pQ>bڵ|k Z˛ğp*8PntH ..ؖxM1i<<'Yⳑ} ǤK(Wϧ(ȍFkЏ#Z,ۿ\\;^IV٬1‰]ϛ#I|B�_(,~BЩ3##y!*>(Kmi]03 ٻ3Z(͉*e[_Ups1�E)RWsN.t\e蕸^8~k2o襣DG("pX}+nY~k rڸ2ivuk#3r7w�4qأ(0r\*B'#BZ(0{y>Ἲ�zhhb626P҉"Df'6B!'JN81p7HZo[sKdU"^Lu�Z)?mT6֏2;(dզTb2M&HY Z0~zz$kS<[�>_Ј">4ҵ%,U"lJa.Y*ƅG( O!�n0uw+Zjc;k:]8界~55Uw}Ꞇjw{f^+Gw>+9<wr:m((WCtE3]Lɺd1%GnZzGl7+VYɋR TJ@N,'9B)jxQBJ 5~P@X m~yWWq(SR*,bk/�L �pӣ(�WimsJU)g2d: ŭ,WWWӧgn&8&O^ -O{''';7�6Y,7C qsPj[-Yo-s�yoZ'Y?KRrľB9fR+!oRzܶh <~B5'fOmfaI% %XY#f9eOuQ⒇jMqIT<~i^eG<JpotFAu?*35,)(oeb[JaYAj3lA=K;hEvte*XawRYNX Uh@R�Xk<{0�9+dv)HH(9*)cK-n|[e (hW {لԫ>{~Bs%0Ъ1�;6[{W7mnQWystۣwd% RnZ0'*�Py؎"5 KӲ|9(=؜][9c&LQ>L}&MyY}N0eNԏևEeN+! K~kֶ쵱hI9o(j_z Pr�9y1@@C/a Q!F�,AGu Dͫ"i-?6j<xks y[ /҅XO?UN%smogi<Gx.QB=_(F &/>}0z 6 ȍȍg|jbpoƭE)> e4t:87$t�CQ!/1\ng5&ίru@1IQ=ȑ>Řcw4;5G.IRi/Tn׌\ͨnB7:�b0B[G;)^ GN9a~; dY 2I(Xnlsv=Nm2՜|1 qJ�5>+85Wq,YGo3k!Rvϋ1- ڱ?k8e}gEb9^1+&I r<O&voV{n?|Jyq-)6 �%)R)R�256*H򟮔�`Ǐ?$Yale*Ldu;$mKu&%u?8&MyIK�iF#qnn,*}v(gF~p '&LaǴ-Į0Z "}sxpY{34#fB`фCc.KbcIk hm'֏/7� B"p AkDɬP 9Ee7E&&M&hD0CLeޑ=r>=u$-|!yO$'́cMQmky&n`㹿x&'o]m1 "7@g w}%at٬WeAʣ(߫HwNHT !<:23oB᳈*HS {yx_ Gze|@T|'ֲM-� 6 b-J]?Q_Ĉ �@J(TkKQB]Aon�,qZP#-KţnIP_j8?, \m6H%$,[ @`=:=z sk �3ut7^ԦkJU[?sA3�f,봲ya 6Bk_G%>Wa<~fˌ46BNCѝX4"3Rm[[]O(�w߄qD5Nvqٷ̾a+vkY&dQ?]>vPr&>5(Y7C(<]JzD7g(P0 ~Uuy Tŕ�P(sZ A+x$#1]SDX='B K( L*,% Qm,Dg&gyOz(dg̣Dz0OsD>RG4]|vW]}}2;*bͬa~ƣa{n?A&�0'.b$_'}A)ލF{ ] �&oa~M&;bRgk˪mɜg11e_?*G]YlkXbKMGn0e<}�F?dZSv;~sS75w-ѝ=2JNr�3'*'5L_uUfiRox7c*W%3Yί@Fʕi7y^!K!t[?=B;Kʲ''o{z ׶62Fk5o+?gBUĻ55-mlݢqqA[0�U޶L?l{b ͞\Qu:�aÚVkI Ё tkaEəߥ@ϕ(/ wpq�Îaŀ51)cy#R})ƞ|Ѵ,grxY.lO= 2Ml]ep8`0͘iMBіGBэ 9>\T"DT~B)h~貑@Y5!dЌM=)skI,YNJ?&qF !um�$@%%0SàMcQp4)Z�$k-w55KEQ̞,9RX3"Tvۛ|X`w-%'B%L[S&{D ?f;*�]0Py\aF+pyx  +1SY\SViQ"Y|9E&8 Huv]@86D[PɳR~"r_WȚVWhݑƟ|Mn`7ն]l4%$hu(_fV@* �Тi<S#rÎ&Hܵ<o@-2PQ]&(G-67KN=' ߙ9iaq%g{GGE$Ua E=\J-|_C@ɻ+ZgƓ,@}᳹*OD?ui8GL?<Ǟ~Tu=, IP*zY[ɝ{�A-[ʙZ �*�xQB-YJhs �Z#*E!P(8 1Z嶿V`r N]2t}pM}S58~N�xv޵&$c6xZy^~|fkuU3O_hQwDR*,'!~x|YKqo  oONGִ:o]KƕtՐC\Hsh*Rco�vMȷ͏&ҵ-ߛe\ @ -oGغ�O@&/ k{[b.sϳyO) "NrpJ1,?Ocf+<pK!9-E2}Gn0.*AOlD@*lSӾt/0,Ǩw% TXB6øs삽a@晨PQb>db(1R~�|/]]yUi 2TxL81muЍ,HnqZ<:I_xՂ7]kka;<WBN"u-U.sv|r4vhXs�x4ި_]\5"_߶y~R`9 CGtls!Ru@}4Pkۻhm]Kl :B>?QviI9]Oߏ$Q9%3�-֐۲/Ak*E 34nȱ4i7l5e)|"𓆝tDq(f&KCaFlA]$E^^ �aoh�:WگybMJxtu@/d}자Wɑs/WSjE`)u#hI?BZ7;TH+ XHW̙dͺwxԦwɞ5\q,DR>`q @߮$E#Ib Q9�� �IDAT*G:|cё֢<+a Y%'ؘnLW�LKxnT{ZFőhj�npq}8'UD,�ERv"Y<5 f\ QF ߭Ĝ5f1)6MLL:yR T׿XVv4<}rVq3+�2Caʞmm89K�85}u8toƩˑc`$s)4nRMO@1L3g|�&<WE>31O0?x?2ÿG<My0ċ;]c*zL�GO\M2�8s�E�MT&t6evr ؏Ka|8Jh<LdjMIc4:CcżީGT=�x]sJTr G �3ny_d2>$];}P鏚UԈQV[SG+a%"Tĸnic͈G<Tu?.e׳`))Pu;vwzk7Tk ̱uB?<l2<2g!)L?$~k]~U/ ~W9�1bho?vCntXS ^P*aį̮ntÑ�C[ߘNT굥ۓkKH+[ nJB�Ls ~eY?*Z:Jжo[siMӿsybx]\ӌMWx]\F f)`ӼTE OJ0�h[KLa@WwzMۻOPKc) O ~mo;[Z3~uD"V A>@mض(;n r}YX�Zq ,12�$@�H])f9lQ"ERpinW]��!8EZ<XʷN[P�Bu#t[*Txbx QT.2 Xp0 {CPl<.t e.B{PђV'o4V 7T8Eh(w:ZX8z.d:5(0d r}z)T4^.ا5Fa,1=f0p%0J8\pg:q89DDČvO4ད)cSqFi0qa }Dp!'\Iu%`Y>E8HOtmtЋdĉT`&x-'�`[\|%df:{Z:zZiX6fIUŋ4-+UȳB]*6ewƤ�|ȭsN@?Tו\dS!o'~61Ldߛ8& eP-�pqJS6Ӹ�y5'ݩk?}lh٠Gi'GF\;5Srx�)b`8$كi&S{3(315Ę=h2Ilr0ApQ CJ3b 1XMC QpME`2NU ˦e`ht3 :x+Mdf�ɄJ _P0X?)uj<U.qh,SS~:J#'Qƅb/ Vwo_%麋\dHe %4`H}Sȡ�Pe3 |w'>>8RtJ- uq>,?AjRM97_Mf)7Tv.mCk׏uiжfyY,(;_gK +gfNW3$B8 >w8ONLbfVmܵM4'ѷ|ZRRqP ֿ% z֩i� Ê(_Zm^uu}]]\vMw/gW_Cu3 unwy~gx=kX0 ż[0;"Dm ?wD黢4~ 7 /}ҕ>]oӷf{<d�,`!<bv^k Z4^/ٛeIpD 9[ciWmz b2{+&EՒhE4C <{ �|@T(/ES\~ ,E@jt`fxӹ['xI9 |j|wR󮗕cQP6 ogwyU#>UC'DEj$u{'NR>Ԥ b)�bާU|Jh|9ٷF nҽb>,rmWq#r*9I)̶u;dB볤'8?i1-b짱{4ovG@k`BIb u;Ì"ns8)ܽL%� 6j؉#c'#=UV9IxŢ曌L5TyZ}UY>NJ|Jl /z= C"}uv:n_šO"@$?,Qa89.Si~;MvA;>U?,%b6DL`ӈ:*(lz_P/T "4I1{B 1s~ "�@?D�0D�\fE`g#KG#bi/bgQhSWc$T&KyB5KU3<d{ ~xg{;0y NZP$%(Qx5Dy"R'O ?>(uRrh:NtfՊT7>ڣMsKx zzaяү'0c ,F{gf�<W m@4Ox #W9*y]}:[nb:4�v0mboGf %LS"q؝c$X>br`Éˉg�)9yX`7,g5_ /H4E NnSM}7Yɉw֩< @J{XqyK6GM$z;q1t0qӃiB{A18n5fۚu[ז۷>|]nn=9<yYaӔx]R:t-M n#6s((?aaI �#֚f[?U -}} =8)"S$ٔj}=1K6NIB;Dfm )3"9G%4r5W0ex%s ip3聲Xt(Gle[s<N&k0X̨SQO['a*x.ᵎ*=!FD/xط%~lgR_$IyҚ. z:u�6Q`m:Gg1R!(BVk a_%8.;. yag#Ry6`)5&1 ѡkXA<)qϿkvvLv?X+ZΐlIqn;Sl5�l` $dkS�d 0lFNw S>sTB>@Q@~DɝO6+UFFD}7uBnsW۽9}ǂ]%gTh|wc ]+.rsAC*yX.86]&wttݞ20|u2ٔ2Vr4Cy)<{G>iv)|2x?&3ÿi|kM},hpIagE `ȅLç'ii[խz]Zaen s*ߊ*\uw3ŠWQ CGHq��KŠHBM�0WpSʯIȍ#%@JQ{a9b,~@i0;tmLJGRu1ӑX+1u!~׸f_d<g|FFn8%**$i.+bOY;L,EaIN"::J%_xF9\I D/S(Dҟc 2:r:�lDތδoÞΗ#=ah8{C*�Zfڸ3UH,5c{8%b=.] kG7}":jY?�ւ,_~LD̚^*~?1c}>J6< kZ(= v4HaJJJTS$zJ\EC;t: `t}?i+.e6'?z:}`"\ `b+ZNlLYɓȊk$F,%>m�<M2ONGB)nj8KWvߌwH@r"Rϭr'ݍf0np[=M&uPJHgS5>MI+`;1@ G�1�(%| pA�0@.Y5=P2%B2$`Ƀ1rM#$0/4,_ O:ǯXrQ)*H6LH?Cf5�NL$\v3v(<Y"<B~-IY͏GX_~Y0SnMW$/P9 G?TNYuq~Rqi?ױ'bKXk(۷oovD iZT2&`8:SfX'�v֞dt�BV�P+Qisہ&�3^ƛ fzTyKԏ 5}@[@*a;0ݸwm0` v蛡oe.R!R֯2Gd*߂ծh0WLUq_^/_]�W__zYQNgJ>?yCkzH~Tϓ#B]c_#qD,0zȞ a4NhmmMg60!q�pQcmlOeu-$ 89ŢvU�+a6kq 2QrGg3x�Āa7찼 #yx‡(̈́S~,K__X<Hh)?i=ƉajnԚ4.Zp0j F\/ΚjU<lB숏r|IRѯ}ÖUa/=` HaV1Qߗ! 0I,KDe(櫘b׶fP(p <�`ܞʺN2~pl-0EY%;2ܸx%ۆӈu퓘Ng)O\4(BDL0ڸqqD<̈H\lNg⏭x; 2Y, a3 v9헔<kcfbJd^E<[���xij~B?�y2ΎvJ|+߈jh鴔wT�)ܐ1O <dh D|(x%3*yrT�^}i gLCiteUK{ֻHɀ\.֓3JH$lP+c ]١0k3ל%~vx�(O7zDO1S:{/ic~\gPTd-%*Z y�șP\dKTa}fh^x4g#H]6 R {GA)=t4=ĢWb`9eR]&4rmP�(#AR nK�N\vfsXcOsT zlDMZ'[ӳ jʐ PzQ BoNg4DI`ƃ'PCv=S!w6y혷=oQ?PWe5)ݰXrsG=b +�r-\:woIuviJ{ SVyGBbnSճqx�yȄ:cv5Sη:nn] nIȘ|!"sxƙ� :ɪn *;_g$^rK�P0St{Cs%ݚmn%;u5Ss@;EL" E-"Q:r(QBUcCDs彽/$Ǥ@@ڜ" o+Ѯ Xfyo*~hhST4)/s*TuNlͶȭY4i z�mhbvvL=��MWȪ `IQl Zy\B)vf7p: WQѩ `sy]ۻWvKTP*Poݦܵ>]&"!t^ۆ\U}))y}='*E㖦7~< [ u 㧏PhǑCs{Tt�ѮКJ@c 5Й9Wwm: CF9y:)UeB5 Β5wInDcEQ((_!o sm춱A6Qc:ߑy>FgR[fs_ \ReˢtJmiWàEP[+?{:0{l1PZ(/R?˧S<w| XB;Jop:Fb@պ�Zjk#[ЉQ:�i|64W~Qv(>کfZl-J-Q)ښmX~@.IGR1~cR-D۶~ͻaS7榹 @w_{mmo[C+arW�wV%Ri} /@U�lOOO|:BuƆօ?zu2mhe,O | ]R͢bw֝}+t4=(I>95:s-ਆܭ顶%ڷi]77yĈϨ#WG_u,޶ +s-X}C~|#PEGs ёD[R1i<g r)z zQ! #&&NJL&E?L|8Y42r]E )M#GdVn <7Sw#~\.П>1M&3[Vz.%U4 [ Y8Tt #_ y{l֍ }mqno=j`jNQm0�nX{)6jFZ@ v.ds5eظ^ܘa7�zDyd[w]jMo1]M:no\?ao]%1 WC_ rQ\ݺjCt6׻کݿ썫o4^{PI4=Ɉ:٧oM{oϗv̎w<h+_l0,l,k,,ᵰi, faAX pwޞ>s<'+222"͊s{,fEO<n-Ez.-9=%<m.%Vj>Oe]x$1'Zzzi>};Տv$,(M#hC "ߺ%F'AZҕYjdnm 7QA�mr'ZR{Cm''$vNGT /Lu("hM7JX;hkPglbۈ.Tٝiuu[tMg[8WǶ*ٳh',凥\!hC�� �IDAT�'D&� OZ4j*|~gfr lu:wF~ bN~ŷ.G|jY4V-fCPEQTO:' (|EupO;J}Ʒ 1@30>Aw-0⼟tu03�>5D&e3nu:l7!p)G) �gdfb32a:mY/`1^s~`1Ѷux+7`l`Ӑu`iz j?7Nl4In-�;U9mUpcʄLft8ʾ[4scA=kB-L{sjigݎ9gC w:K BH 6$A>֚cd#dZUq, 8i1'#|Zvu6 58ǜ`GBh(#cfzfz1$4. EF(u9@~!K;׵@ACmi7:<cyV6\)ӳF!A 黍 Re`L'IR�YvQѽzm[W4 A!J .WO|ED'?s z>H3;GV#;)&Hp1O߹d"K۱$|~VeT 7'J\ODsiS/Ǡ/z..4|rL>xM?o~?͏`miYVbkrXJVzuVMQ 'FRj]A=1pc2ҔsLޠfɬ"`0 IuƖ[4=H~�pg7rٮϞWk%�3gRɼvus]73.9@x<픦|)Q(�u;7 OL}Y_cxnnOw^L)lQ{UV(SJ)J6㊗cKWMy5;50w$%$٨l$)*UXH`6+_֥IY]_3)سH'/ DpߌxImW? \3Zdà"G<(oaaѶsm�Sm:>z4)z. vBMTuަk%hem}a$=0FϺ|ۧ.Q-y30R#vǺs:1[3 GoS(c]b֝-NMߊ"EV,U tr*vGUyosKnPb](8KuEvrϴ[Cty)2Z;yYpʺ}Y8n7 ұ$l=^&N) VgRCڕJ+cudmuSG0j Xi)TE 8]@L|: OU5UUr>m:c"9YܜCաnn L?R>u,QD�ƄQFGRGʼn;o�4)kPtO( 6\ T�q`56 GF�ֈzȶoΝk�2pM}#ݪVm6ګխ IgQ. |)җAv̉ zƽ 5 62䟴WŰ$EXT@H2ND mA*Vfi%"K\�)G"KgQ=<[f$.fz?'nPG~bJׄXɌR5v}; #͞OH0bQY"ݹ?ˌ:[x8c!^e@}n\.nk|W55z>Yk]keӺPԦ QjBS"Ɲ)�0i=$a\V$w9?VCV=mNJ+]4'EZUnJdR�8wҿuZ7LTyX7UkYZ)U>@rD<kʉ?My]iNM/^}>bF/qx)!cZ4 i,FޫtM\j?e<'+ P"|�v9RnS Ĉ Mab yS�4n=8P.KiX>BxA1&‹L"9R~+ E24ٔJK>K1�8]hP6%lLY5�j ՜m?�Y[ˎfJ �*^/ӵL/6sʘf`B#j^+t81ZBXQ}*J+Tky)N-v-r:^ddۿ8lx,9lXm/~@!be7(V?_^"�}RHu%|N 5nR)e,g+~nNR͚Q- P_ )fӣT`ɮ_vő/AK'!.οǧ.| N_M.+GGnW"آLUoW`23;GUxOjEz(3nd}Кef4:Y+C,b Hr~nbWO ͅA }P3زDrZFWyF_ _J{(~ծbQ|@�y py \LNXQH)dL)9PA),�J&S @Lӵ*hW-<Nfr%wǮ&F../g`)r¨5&i>^SA*mgI 3!yL]St1mNQk'zKza!L IW�#=ư\&)_c9.w}vD{UU?{^ $`f,1>�w>6\s} `C>OzdZwp}|QV?ISNKO<y|"+},gN4+ {swcla 0KIuQhtӵZ/<(=b <?1q8 AC,�,,.";o'dffR xOV4EU[O?;EyAk`;[* fxF^}nWL<JEi�);x LՇHs�Y"Ҥ(4-%ŜYleW5R*8`1UiEtkEbU3Oӈ3a `cfFDZ2cvvBc45�$<,P,lۭRA.f|5XUjՙ4`Uz eI12C�N r NJy\CkWn~&WSQ+U?|rՔ>;{wnjm#/8)sMU�8jUeO}�\ڻ>]^*hsQJk22DN^ �MSWO+>T>q$0bQ4NN 6ڳ(OK2̬]ty pQ=9Lh\&~.�X8z{t!Gg/t+.@,HO\cX1Zf KVDjpIhpdqd->mg1�X? �JX4&xE �ǩZ(ٍK��іM"k9%o41ϓLI6," XQg'wńU;~) wiDkhӪ C R\st _u1E>2QS`v6 [i0b@Z&%4,k3*ɈEݙbĜh񪰄OvQ(ԸUH#e4~b^7;)+,2v~I3J>7U/4 *4<~>.zk}My]}yZsZj-.+w}oȄkx̕k~m'Վ=vEOiϕ^6( (Y&6<wqq 6:Nab rnq7G_/Y-f1;3<f ȯ,� X547M"(3�ZUrw'?Qcw> |zpڹ_>D '&:J(]®PhFY I陛q)N̯dW/ڎ0$b L"rIԊޮE-!# %$BH6!v`i'(}J'6l;p7I=�ytXES1:܆qdFy+s8kK N|a.?7ƕ+"Q`[isʾ,U{8i;53Ji2!%hd v#KDɿ%R<n'5IsͿxPK閪 ;ҦE[4'm/SQZA癝WȎg0H Aj Z<,FQF*O|K݄]X|{( &�N ͤX3_<Q1ǭOA=2Ձ)2RW@Ԫ 0&`p>Q+ 2ePU뢝�JИRrJJҤf{m//4 hn#"06ӅI&Ak~4GVT3W;tʇCad7A|v}�Ś% `E{.iN(9sn+G٧<*̬lcKjJ*k&͋_8Ngor5f}>6_eyޣ<yx:Y*_c*ϒ,!8M9Giʉ9ђh֘6y4|5a#�'Mq<>_CsU{otGaϭrG;Po l٠i,I7ꃴbÏvՒOO d]{c#JZ,g 毖ߩeԏ)ߙmK<>�@<l哥YSxѹ'1\;hsf<y"W)�)x'IaҕIvlL|•qQ_,K,W:!W*HJCr$<�O[ F^ SWv6KD,>) 5PF6g<lyX>M8ӡ|磸- |ĜpR,נ-_V^R ͱXۺܶ{�"0*@4aNY&WDTYٽ:rr M0l FP;4ExYhŚ>˒t1sIt٦}׷>.*a.J1}D+}(8@ggGՖJgZ7iMN5HlZq$p~k"MKA0JQ} &Q4_N!֛(hq׫Q؞zuڭ1[utmݎg'g?w*3G|�PGiD]KjR4U\L"�8ia}I) D!]q`ӵ CF%/t3)b*Ir=O'Av qU#]֫L'Lلo;h{8Qڅ!3=(Jb?VK9ut|>[o)˥ff,ѲC �)fccCJ2Ǥpƾl>Y! 4GRҴB;*MM̔ n�DTnt<[@QIw֓ackƨҌh#4lnj)(LKȴF;^CdEܚmnK Tk]X\q]&+*`K4.^Pp҃l6 _$-O^dŸodӍgqz iVS 2{R�r/)*z H5-=>8;=i-J7upڸM>5הU*p�},l?|O6fv:Ի)3DGB[MЕu mǙzS4O_\wi"Uoz,WE]cdt{o=u1ɷ};0ʇ4楄@ȦѳQ1\􆥝).!`P9͙)h@7DBhkK9DP`fyA|d:8 =NNs�?l�Da'U~n[g't Xv1ٌ9!Z;3GIbx+<"MdN29aRXOV P\Nm0>L\Mۨf S6ee7͒6VrT~ @c# Kڮ@K$ F3*oxhexl #ufs;-"IYv`)KءْO7v_ŁUuA{?jR� az7 Y4i *�P:p.ڬ KPHQB3�+`".MnT1֦ѰJ-l,�Z:g(u]#Y6͹ȱZ=U"K.hIlli\wo4&Z)mY & QvӜL*VGȤdߢ[cLuı�D 梣T~:^ϛ|}gew8Ӹ}$nO99)8iVXZ7:Kz�q`O6IdFj4}u]!sndRT THFt.;+m7ͺUWf{ܰBC"AQFi�DbF< uRXKęv`w�s[&J$Q7)DkG�xQC@iK2T2 �7j;V@[KCmuc[i@m\ҭ7TmV[Jw>I(EkhZH=yw ٙ. (@DHCTxZBlɓ*y5 'Y0cq)5j�SǧmC>˕V"eY11` u{Ov&MzRrD:æ=}&@{v j]Pm6q`pT� 4;$[MkFvva!D Vik| L:0=is1׫z4ar$j9zPg[�kL}rvo$l?؜Osi߬5Y'ū\#Ypz$|sv }י^@TU)1&,q*089զj ײ[1Y)mX3xsao Gfujt{Y$ʸ0mÙiKv (TJ |he*JnI;gA-Mf=CNR4:IZ $i5 ?5LbBJXhp4FM"#y52a�*d\!Gc' лQz9rv�[O4Q0lue |ol}[Zi?Yg]HԪqV6i'TEL U냮to>Z+àbQ*��&ӆ'Q$$/[kz\WdX0,ut$w΃'EecMkBhvBMc�4T~L5ĬdrʲR @(x-weQvO };Z{%"|pF#O7YpۦOmXUz*lh6cWN<XchQ"sm^麱]RӗvS}_ڕn`Tl4>npJrǺ!i -7RL:;=9EQi@kWm툍a$ng?a'-N;_Yx!K�� �IDATNOcpz|f&3jݭ O[+ide8.S( zv>s&ZC}LHXi5-ٞ:~/)Lye ePMqҕ:5k3Jؔ$ۥb@[q4EVzC(mXdz@MswUc24vVTF1vCB\$;2Kp@8J<}E0bZZDkH&ʉANATMcևΩ�NWmTyެ ĨUo[~ڬ36׽�U0dϻYNf+{uWjq' :Y hJw3~{v xO=z(:?i[[?/^n9SpYW'j7%ˣ,�(_eTcwσ>^qTj"dDs8^T 0 |jF%q tE`726G(^w4n;0`ZfMa]0rÑ8@j)7yNݒy0Le!e;?#XFdBY8PE$\A0չxRcm!vCqVMP)ֶTLm,m Ĉc *WeCa l؄saȄs@VSRuWK!Hi+sOz]20mf,qo8aFP#g騬Y6OJ={)0B9LjUwݺxQڳuOp1x/F�Uyn>=)ꍹ6?\vVŎ+ɷDN�n. +Q) ~0]1/MHݟ(ucö+5bmFٺ?VaPx�bLWz_sVb~Be|�^_]zu~'f^\.+Ob�Є]/lFv"1bަwrdpIQG/{}RՆ _]yIoc(Mi2{pJ= vQq&kp!w!OiBNǛ+cDĮ>rK!Ơ1�7tjM u4`fq \v|Z$I_N˽E9)x3~#m^%(+ZYf 򙖼H\N٢E!tl^GrٔAZ>+]ԣ`ƩȘ. }\Wp,lє U`ϖ�rlR[>+q"4; lc*IZr?*Mn,RP/EӘY/!lc|GED7߉y Wb+6$"D6@exex%%?4Le&G)K?)_4"MWj|<}UB}[9)46K_ )e(,/Ӏ\p1/3Rp) n@R0mӮ`Q;%d"/SjMk-h ptxcUk,934)&(.C=QΉM{ĝO" *0F wsWF\YE2qݝNܱ76F3q3۸:ؤsDj*W}Q\ٍYK+T6j̪q11JαFOmBs*AmgҴ3gm-7%M# r]n4)#O<>CorQFţȥAء]uv omϺ8nWznvn:?c"Y>2{uнl}cW mCivu"dm(0&V,nv>tZ~??IK`c&8'?q Bvؤw%݉X,*Jӛ "si:"R#c}y"-!^tzn(�$*>Q<.^<yK Ԙ?*jLe,(c,eFʡ;v@؊'@i̟lgO Ǽy0P1T@NFKHsU`T4d<A!ٛ,rL^ūҦKX(.Zd\g)lNJݔN1M|T{fWi'h Q4=I>E5,i)wW@烙5 JzGd|-]ɍ#O?\x:s;I:OJc(fW֏SGܨWgj,M$n^"FF>3|bEkd)\:rϥ<#Sex0lo&-vݶո']| (Ҍzӌ~F,*HE566K8r'gz®IPj}ϴÖo魰/ yvDU0 IX 9gQو< &?8Lث6�i` ݘp &/M|V8жl 0/^'T!PA;LmX|#?>1F~j8k[@ܞAq�}lÓps)Q0.6'xw)Ot㾀Ue<2%;mU 9uuG9 Ca7(?remxS|앑J�9l%`@Y*fz=`vX.=Zwc^WVi "7Uso�:!v\x6auշɴӶcpNm(j= C2Ӌ6y=OO.`N�&;4# p�-1 B~bT.?/5sA6 T20-oV! }̳@,е چ@PJ(z(7ͽU�!?Јsl$|2M-F\#߾r](ּ. 76ɧО#p0M3+YѪuoed [<[Iau ^P=hl^`N&7۽Щq"H=5p}Q< 7M?IM6mH7H&~1 /=# �&]h㌌R5ʯZbl$Au1O7?.ceޞn}+?~ Nlb<WT@/|Gt,&C/ ֆRKE쇰Wuw֧ʁuKX(CATVX C8Ӓ z&a%/ l}JM#Y\HK$w!')XIUqohty /qeMłl()2ei_c:푴O<( R3d �v.芊XF섞%ž�гҔۻ�,ID"XŧѦ)31>y\NЃ}'*7n̝i �tĪ/W$k^U,rVos_/~՟֟g1Y[;> 6f/#.'ٱNO[ ͺv׮ԾȧT<p4=j'0Ys'lP�.䵁;KDiٚL|_`?*>]It>2#OoP&fR1fv7twq P ~RYlÞ0BqN)ѽgd ڡc|V!JN !nOͪzV)xbgK #!('|⪥$+*2`& e 'L%YWf{~ زXOD]\DxktVd5h 7\>':(]Gٝ< |Q�e"(\S;#!#ŵLie).$cCi.tzx|CNO;{H_GN,k/wˊ;4.h^*@(Iy~<7O_=/xfsשv$Kq8]]�ii)';isaX9Ǚ)�T&YN+t>]^l'4Y!-Xv?: ?GjWnG];gW;j>///?zk?$ew}_ׅ.Fƴk3eS-?^tR[ָ|qIC.uQ jЪ^OFG+%}j t"a{&.D L1m)ZSXI4kis?y OLtd6q6ִ  ^6:KeԖ6e0!Ol$�FzQq%.AX)S8Pw80aL!L}f3"?51ɴuO"!Cik^a`$,F<:z�~ޞĕ9D,>,ئE^ z<q''I`zxӶu?_>g'Cq; G8Q>+�jpr[Lkfw ]/)0SXncyĵ`p9e_%6nGpC;ZiJ"av^\M7\76 }7~ uHzml݄EQ-_՛-W+3;H;|</u|~U {I�xE$M{0 g28"gܳ�Ew`ēkdB=[lQ K7%0tȾkCL]=HQB'fa|oucD$@n}V{D(0wHX1E]Q( 2W2F<AMS_Ɨ>JXpQvTPvv:W@'9Y/mV,.?3 ^/~MB+'Q -l$@HɃ-}]VF$o=0)2ȳE' h[8k;exV?RF.k#hF5Ru/_vv<Zo#8OCwCmnjQ>*fHf\W1]"n3J> ]WmK.+=4ϸ*IL`þ<NEJ3<^v|RW-?wxÞ_A3 3 &U[ '<vjlUB(1]`̽L$ 4KJm/!#!u~$ "H:;!sa$dHCsVYꦓT&~0\zV$a(ÀJBzgVb., vt�Yv,PF'j,ZJO!D?$@lsTeg30@a%UC# FCے6֝l.lN7zWAS! OV?Oj=$ 2h>9-\6 ѣ+W z# DXE$r' hY0f*6J8=[6JW*.V\{}ŋ</O~xھ(˳�ۧ5e"%Jͷni*]49ps$ �5t sLި6>qW21ȆLU6U̽!{#�oueuNy o #^~; }ܖMukcHeF;RE$Ռga_xY)x>dgюBED4h Ca2.6P#7rknfcv~ˑV$ fQw\*bNڄg"P:' oMb0SQv"�c4l˧B_苒P&HlL\ Y6ZS<|:qU4Sq`*(�Z4UF!l#ئ#h `a>z63}z]I9 i�."+!onɫxJ79� _/ [٘5( lE)0[**)Mqfnv2Eی�Eoo2mFe^ 2T Ys`T[/|`(pe2xWiv+nw 0|<GG#ƛp*e볿qPa{!T䗢KU@Ky)/i y+Iɉ% m_8ƶF6H=/)v8xs!�ZLXf &n Z3a�e<K%ir J; R*qb$z? E]Q},]shY�#G$|� śs>:}>g% M5G46~;~䜣4�h)ZYv "!);DǑW)UzRZ*8~R6tLug7F^k;Ϛ|63ZH)9,.`?gse>lTfeeexZJ5+MώJqEH}ll?|z]PH`ҷ+@'fh>HIz7wpu{?Z��<?�~a'x%KNoO'a_226KxdbQ@̔0.Cu5�x.eXyhiJ"TH Ҭ�̄Yk.g$k72$8Ik$CF7� $cڗ߯vS'JVn8W"/|4M�]SD1wK0dohDL1 zyڍLBl ((rX@⼟Dl1�6SB ]v^hPMcR79l N8&K֚Vcj"`n6cÝ=S$􊌷gF<"c#> 1mɪ9={IgKxj6C] 0מjrZfslLo|ו1nǝsQd 7}:;Sʐj]GW%X_1Drk1rfI;8njl)J&xEyG89̉QRa?q0uN :�[?kܩSvW?Aoʿo//}oBg5кʛaLJsݩO9ܖ8),}ϝ>giH9^x* 7r!T*.:}!(F+E;r �I̻m\&|saBwrr;*hӞ&4<ˡPw:R5=m/ِuN-ěfs̶mgYgLi3>0Z3s@J xM$nYo93'dW!WOLp勅^j^v�Ҙ]\!b4 #)VX;EE9: Vi�Ds,$+ n+p"CibW9PmØiLǠ6};@tܾ͒j 0JwD7#Қ'(+${4ze;~F\OjS,>`\�tӊ$RM[7~w{Cs R o|)e @e;5�mqL__Mx׾_k3eʲ9�h ѓ_[-m4S6߮HPs?++KrkmӬuVWJ&�HX `dy&o Np[]w؎o[,�$R4JS_Ͼ]\^ǭ,W<o,|_odY ݿGT7o8۶~_-a&̳.Ɇ|`c�ǯC:B2p!u~W^:/YR~_nKh! Z�;~\k.o.8�� �IDAT?{wW,d/Z,3Dp@e؋4k a.9ңU,�6mP%ɻeY[5<�VA�yT yF5 j_Ͽ<fN^?ڧJ]?eQW@"Jr|f�$|fСX˓0b(c�V,WP,�ܿ3PBX"@OQ�x--i<m^|!cSP>|Hf[A?¡JG?KcpBm(JHO{CMI[_x;[i_Wr!~?{HLLDe" @ =ht1]͞ `} 6tk Z`=z'Ȓ5'y`AИ|\{-#jHԐBV2xQEH++^Q'} *k7ʹ߹{;O]tu+7_VZB)󭹴3|u=o`,Ν'$.`sڍQaqtǼ1xeCGO_0|۵}SkisTYӗ<~ ?Թn_?A=+fZ).j̉Dk̹'k/~t<}Y noFI9 D<1J^ >Qj; Xy`|�TrmyUFNrm&ƛml&Py0L"|}ECr8sH@p,80.q o{STYX@HyI[2eIf2"E/,dQ s3`#QJ/Rh鄶Xԁ a �a4k-˭n �@Xn-g4 KǢe�ېY?8BZ;V'ne,&>:RY"It1'͜!:> q>hZk �^ag2:.l`s'C_`]uN9ctaqqnqO7*I|=Ϋt?ZZZ\b=("m&Gh4xdTSah 8UDݴO�`~ql~] 6՚L]~j[Z.FȚ3\~v$>S#7j:=}~?^X|E%ɜOA̙s֦*�~sdM%z"�@nqխ6[%)=BWy+;Vz�cm76y5`9qS>L;kE]}8݈;߇>eCk{>P%X3$)e7TfC%(c�$Gu8 RNd?Z"a*1&QcV�lƋM/'yMcc5 & 0H4/r, Fr+3@&2VFkVsiPZPFnpQ3`H̬Z jr94u}:W_ZY[5;ٓs󏾽.w<5VR8^ΆI8^\<ۚ(j;wnaSRќyrC=Ҙ8?bVXjs \̂>2$\'>4m8R7Qk:'ӧ-L$٣C]ݙb&4L&g>9tUj $w4̓J\G)'{P}ý5U/ȴZvmjUY8j/T$`d17Tt#x\۵ �O,SBK�Ũs,(Ji21)Ίg)L m�[3wP*{>3|Cq6^o÷ 1Ʉ"0$*kzOB:-+qz:<{ę($FD?CBcVԕתDjfS>=.̖B,}WI)ΝoMĵ믿M I'[{0| 8~91PU#*9RdQa]~uRz Þ/+#AcK?9:1lTy^ hHFgu /O}À*醙K||s4N}]0 9tu2@vh ƎʪT}D-id^O)9$tl=r@iYF^K�P'#η>dzT BK;7>�88s*D�"  PL $R%9ƪ|\'3FAaF@(P>~۷Pc>1�0Q$c�J Ts2[5͕z}4Vu7g%_!VGVD WzuT=|}iEٳ՞t/M[[&ƈ)~\5_iix L"Uh� Nvb}d ΞB -,ռpv..=nd3fgO..Ū՚񱹧.]4E Gx!S~OÉT)yգYF#pz7Ybu!Sdރ aij-,f^6[�m,`&Nzc<'s0VSDb =D�v*�рUxtK |@lԃس,Z;cRa&  ׬s  nP} Q)=~ty?Q ~hf.Dz⺫0`FGh=6M-� Õ3gZΜsl٩޳D?J~sqK+%g.?[G/8R$$ Rsr_2"#iJ)nVO-55/ i~ZYzMs#<S� 3ɩ_|/5/_9wnZ3sǧ#?}{EWk4|uj dT2Q XTnic#]JX"Z{/� ~| N#KKjQRn(>0޴"q3NYj.gs>E2K�QٕEKE8OPd#-KDI,`_f �FǾ*+VIyY5[�2A)U�M Tq hPuʫ`+�O9T cDhIǮ~XܜZZzN8=ȩ@K\scӋgͦRKGKavT(G暇Jd(J\GW[k:<rfvdpH]HVafO>sFUƺKVk:szư4ZpDx3-i[VW?Wn|}VW|/S,--URB&-|@åJJUӿ;_X`Ha>8-^k.E(@X"ᅀø>Ȭ1*R8Ţz>e 4DW_:D3'8�)}EYK3[D5.>-(2#*h�2qjB؏PY3WjPQ~l5jx��IBKH*_ hOU|k\':e~4K n+BhƧc;n~Ar�4%[\E?-,~Άy`sb1_íYO!F93/T+׿{֓K*Ub;)Z᫯^5}҅ ) g7!w^ʙ=3D7ͦOzqԤ߹#R#Mhj hu:[z;ߢw^y񥯿UT<�zT̯ [I-P~'弾Hs"n7v 8S`)[4GDG=*,jo'Uk:iie �1C L(EF~6͛ ĪOD�*q uY^mid*`ǽBIZt%RI9 }z09 IZWCO!L 8a>�D!LEi |Pؗ Ί:#L;n|:>gk7Lbiޡ2E4?_-�*Rr Gq+lH7ƥ7n~{mq)D5= v&M%U:[ko~\Sk:^Y9D8$@LͫkWoN̔*@-7^~FD?fs(SNI.SΞ뿷|}me[\\=UmEҥg`$-_7u TRuѬ䑹1 *Iou6vCo_FMc KlJWW�ibsO7_~/ݗ_˯}<iYbQd{>NrkR+^4 EtCPJc+ @D>֌B 1 0q˔T/cI)CiL2s|f.>J)RaRE�*BPf8ji@*ji E�H}�Scu7XAHٌo %DNB]CAːDtoXIASʼn\Ś˸RZAOַ $xϞ>}vvfwR:-,=2 Ûnvڹ9/T J{kcW/Rg/-7ZRkkon~k_4]\uT 6'fJ-f6޸~xros.0wsex`fgfΟ6VDZ= @I{֛kKVIښh}s5#^kdp/=bsg}kkH)F!7U 5AwRxf;MO#33''t0ܳtRS8N[j!Hg(Rw2(|n1+8Jg$whZ;�F'!e+rTKIEqOeX{@]=!FGSDQVB,*Sa*sa~mzہ@4a`p5Q (,/k�0g,0^ $B ;bO]yna>&*5շH"(C Y[_mom0;733^<`6 se$aK;\Yi˷v1ܿc KyDlL]\ZjyyYX^U3ryl!o{VnuwdZxRµ}c핝].61ϫ!.Z ZzUbzgGY++7K1׹eDZna+LWN*H՜&)S(b.7tݦ`kJa(2t[.GˆTe'&lF RZC) 1&i%;L7=|~@@8ip"T\0 3L{u ǟ:s�4/yi0hMT&.EDjI8ЗK TAT?8PaQU%h  }Mh9[}-43: Vr3h15|XU7P G$?{쾫L/\9k;wt>_%?\T׮·/>27ԟ<t3e&v{Qv6Ξ<u>i̥ #.^WE 7Oz'.Q:{p O]\Pmol2پRUNMM?!1wR1O9GbS_􍗿F_Z5753s/ӭ3^2 t]jM/j&%;S&I SՖ#߮.[~0HE_ȹgtggcw-֍br (j6 Lr9Yn{F0B[rOX^.wa } "i#wL[BɪN()GWٶQ,?t+g]0K9'QgEuF@ 7p }9j.�/:1�=i wsj#AՊ6W:qK*'̕lou|k:,{'Rkoc2LyeVo݊\F~0#tw+k+ߺKz4/?qS]0k>jemuN5/}=CLШ[ưې7fPm;ʭ5DZ'[/ͷ1!AKߛc_|vporCV?Wnܼbb'ujƌ/AYyXy:_f)vܥǟt>csĢ�48 D+GVLdh73MNf^*v"!韻fG-Nr8/D Ln[v .�ңgIn" VgH83'=kzlFЙ>caR=\jğ~/1A^4!'Qgb@uu8JI$�8s{^ Xaê`P]nKKbN�8f,4 5\xzì8А۳Hn܅ @sZ7c�dawcꞑ[ZLųO=^0v~(d;:oZYkz~0&|ietϝ ý|cVԩ{Kn ͜L-;�x��jvVwBsߴ&q׮E[~s?VoݪV8;{R33Z8Ϝ!帹v;;Q[kC^2ܹGkM'Nw0*\,?}D6 K{y2;\ԉ'>qbf~nѪ@thk%Yf@$,e!4M3R%Ek8DaA(RiϦ%}ՍFq6#ws&X=}"lN�']3K ?<FRk�劥i `f$URJ6hz#qD6PO|0}d:Y^YipiE"C=d?nH{`=: >O}YG{ྣ%}-3 vĞ8 cx!JBt5l~M[}Kf/]F_Z>"'TC]>\v�&ZW|bshM<ǪYk`sxZ.}lTaW�{#S &Z/~9ܥ3:KyKGGD2 .XelSkutR-rA\g_E۞~rNL'/K_y걄W`d9*Ņfs3K^w4di%9u%GI`3ጪ<|MF}9UU1i-I9^}ux-,8V}�{ iZ.!pJp,X�*ߊrkЅ~(<AE sK3nŜAhTMd#p*P#Ϥ(&UrfH.)'L}DbȔ�DFݨQ)waTbêH@|3qh"P�!8Qudo"<]i}Ń\}^]yH'yŢUl}�� �IDATvNmnxjOcC/Mg� #s3'Fea 9zE4/t ηZ#*B}~W_x[~ql d̨Ld Mͮ>K#UI:2K*s+WʋQtg5OqiI)F.S;{(wV>59U]\[گ=Qk:cp3k^{k[#SL4kF3ԉ?X/pjdcN1 PYN`{C݇M{~{cU/N%9k\>DZcZqL 0GycD ABԅ^9Ky �Rx�3@Ru�8Y,<oKp"Uz!('(|u@QZj#ItjJ(&8Q?wP}R"h�Y$lnEY߆;DK:HfX*10[%\Bq޿fTkـ[ٌVViIXƋ`z SvӾ;fHpݒ533'T3t)5<K4`{@cӚ>w.M/^ս|&r�w*�YҹCa0KG{zS..9M?DZM~O?mg~ۛѬ;wJ�.^<F 7_u'|O^rgemugWaVc[8NI"PIЗ>}/K_oz]&>HgDdµ|ٮ]ـED�xv .$R%-,)^-2d eD~' @1LNyJu$J*ss)sRE ~ L}HIif ;c mXHg `Q'}�^c=w�rh�M_W�#KFH7QjhCkJ5l$ӧ\9X51 :ۢrXk.,>v jork-MN/ C i=wOv?v}G_x·_9vOVuS,;<ڵ7;pS.o| �{mǭ䑗@;ph5Bst[Zz4S%('Ν[t`U&SS%o&4o|E"koGΜ0י;sRMNU1趩�J_a;)&F1՚3.|3\ƯAh%F,pYڳm&"%ْkv7g< &o"E4Q*Ѕ8e1Ig̩Pg��%I�*sHf ,%3P$v; @A_΢ 〒I{{l3#M6,B_:s,Bc>$|6"� 䵆 %1^* A6@!dSZSJ5,@Dpr�zu]$BٛWn$zJkofz: >F7W6'f^x$ ? 6jyL-.9'^\U\g.<ą?Қ17$mNN}.]* ,kNN=9*vd:A06?jvfvvf̙sDPBk肨Sz핑 kMO<2?9}0OI~A%Iz0vx/@b\xBIzOΜxf}F۵Sy4]H7Jy`<k? D�Fm W]JWC(DfELcBRR P^"R�hP8?6EI�Mafi@ B$DAЊb8M" {VÞ%yG[>d_plƄva$Y]SjLX(TP}փEEsgRd$*I7N_| uV_fy҇GW V{*ٌ�4W2go=uRտ?r7 ]_lG3]^ź^S|B/]iRsq\~ w7uGYQTbw&zI؛_ F(_O} o|dUj$?]5cKK/}vͥǟ\|?\"B|泟50o~KsrbUU!$~~~8DhH_\:f/ϟzUݞ3cWY ׂ5mJGxF&Ӎ=pd SLf8r6pSn#:*8I"`�-9>gTaP /xKgEY@1z#P%/_|>Èq?6EyQ~,+J'Ye(,�4tW%0/E<XrUx/i<C)j@ÅJPv~U5�]}} 񲹃ƪv{ M?tų(Jһ7nWlo(s;y7WG 9}_ԯ~wx}-}7;;;tjgcgۛѝrb�'/7^iZJunowxm}J7'\йs 8K[ c;ٻaxGY|5s.S</'NzA_Z*7Kٗ;5)f#\g?'+۷W/,Jn{ߩ^HR׮K_,G'I$+_~ 4r>"g�qx5^=_xlsL4cht-�F#@c �^`=|j#u9,%X!V<d<{x}g`K4bqJJK܀,$y5QWv$0?V"OR@5Z.AW8Xo)q2'Y܌P*h�F$<DVl&UҔcd_K~�,容suyX=̱W~uo] So^mAoCѻ'߸xN;isB[*ޒ4Ծ}fdɩAշ^}Fso;w{xKFڵ7^¸|jom+7{~+ޒfmi;wpo i)O)^']vdpvel�L<η6nAKu^\Z΍޺uME. (Z!~ps);17w~׫4[}wkf�bu[_7U__C%eLO<̜VoBA+޽Bp)[lj2 OclC;͆a �φ @E(^'l[;q#35CpC0]$h܌HT73Y]dvı Gf7g3ȂXTDI7ܜDHjاg(�ô}~>t0{\uB)�6Noҽ؈ ӌ7iɁހ}sd~�ڙ&L~O2L_Kj#rdd<[(P*Io b!~+_YY[-߸pɥpۛ;Q~Ν[Մx�`2pwSdi~kop԰}XUtxKZ2Hf1]ZY[]ORx\pOns\X7RuT꺔};y -d @0ftjKtqpgοrs!ԺW_Ow:#xggcO/QN �IW?K}/m}G dS�s_}C.Sp{1C|j�@3ނXq<j7�$f(rUWa#^[vv Ƽor91}F#Vn7㢌8n#gY?8mQ=93ɛ Aw]%whH/|bQ&U2O#w?xEW3|Ÿ=o@絿ҫ>�R=#P=0 /%$�lDoڲC+Omn_%6Skj ۑ {hiN< JkGn#2V2K2Zsr;(\^"<~ dkBGbv{)l]ܐA:>Krt7tM:kqT$0F](λѝUZԘ̭4\wޣ m؝^֤rQ[w@Ws۹#WyZ+ ̽˫Ꮂ8g(6t9zuL.<9.�E(܉7#ja1�NmpOvV:|ѓg?AV_w8|k591A#'`+s,8+rm]ua(\];0{u*O=96Jt,� X͡` NdMթIM 1{YWa?SinKZAqvtcjn YecT%ݎvJ8 KĹ-,mGqnpL#L;)Y*sj(JzD-9~o~lD}OZYKJ@%Y'<v8gnF2b`M�k&:v,%#agY- Q14�4+jQ2P;+j/cV5 vNzTaivAsNO B?LCdzTXSm<]X@$�Ld 6=׮8;j~&jS7׉8 GNr&}*•xxn�sXfT@o6n:5ų(ܮfDj`W2t`w;H0Pbv iZ}L=6=?fKdPʎcGj>T(($bLx>FeMR�*L *^7Clb^7TZn2Kv{-"2Kyl'Mj8+[wԦ挷8.~�^mG�F67D,m3?6v䪻kQʇ1mcf @سP&>0�*>Bo# {*9BQʖ; QՉhld>#%Wl{Wul֩S4MD)Ԟ)ω\dVla؞I.'F7�$~'kh@Pp sN-x,3ґ5I1.<;qiN:^'bⴝͬG$OHJ4`0i>i/%�l,ǁ' Yˮ,n:a?�sns.fZ{mrR dR̕@@(b0IL;?Xb�P_"OUA"sfa$y2Q>h,j-Ev}ASJL@Ť" r{h`6w'2iaj 3Q!H,14~Tͬ(_�M+2G73߼Ǫ{SO4? ɱ89&_d|ܙp5Lָm3Bމe//dc3S];76ӊz̛ f^=cN7loOwOVF|,H^UXx 롫\ Pz@7Y}~!Џ�!tٲ]4!޹Sk:fշ dRh:=n68ܘ'yG>K!Xe/kBi6En<g?!�:iNz}�<ϥeyJ(ulycƄL6m;m+:.|6<9BYE4goxIHh^8Sê}D#XW?}rkLʽJIBc~#{JDiDtjpaS)YF6ҎpU #[_Z�9DL"?&c1Zyl~?}j,Ӟü:�x HY�|D) |2X<˥m;" Myr#NJzrgC375ֺ֜m� ׻֜w"'lW;Fֳڃ4S0z�-nMZi�@g#F2'h*sbrcaMf2Z0yX)t-� 6V\dT<i= @6+i4^^Ճ/&,aqLX3w[<šm��M(eĹ/e$YNFIʜQJB>Sq @,27BNa!|JsP!@D RQ'niPԘFwA�*rܑ La^IhVXb]z#R<4<$Tgօ^,8aI1C� @S�Yy\ .!Kf]i| "g]k@O.-zip-�Yw�7{@mK <`d#)2a:9IX Us:èl�NWv2RW23EawW�O�#:@[/XݳԤ"̕95 |@9|}|&>is' 鲞۩@i&SDCp,Hs<� @U($YREno_==kL h :ѻWRU1W fE{GiOngb@h:>sN rXC޳ztM5hicV{q1@pZcm �]�\ vn3�6ђF{ȝ2(|L5 @ ec"jt<nF9Ox`<;z2KOu%I�d-RlR( I@@ƙ=DO7Ӕ ?#գ60L7p2Kg)+@KyF3v}${۽<qݝp?I]f}PTI ,<TFVFsPN&i�ts!P9+OAN*Z{bauG~m]ٰb gI7+}(+ U�TaI%w}@//2}%U#Ql G/ L208QOKPv2 XT1aG& jyϾw>0xc�5RD8ϊ*װ�xv|U+&Z Nֈ4 {`i"l.1@;*7[&,R7{^%9V�$ڈ)̕d;V1�;[I [Dɤ ۮs|z`a:m?rD;IwG_44 zmY1'bQT�dY?fEܷXyGq�$tvds�2W2AGlV l9G @4 h^C:[v�J˟yQ0Lz!,�06�*�}NX%%PmY9d*\a3PĤH*hUQ�| s>pTч`j��IDATXo# ?k=N(oXA(Ek#@@E( sXb%�H^Rsqs@{ =~iZͮrlѽf u5�<[9;W= @CA (Աݲ8N fv@)'X+9~�;qDž}R&dL%,sC�B+(?,g3M6Á4Y_I �<,gHK}TW}+9�Zz�dBXYlۭl<_&rc>r;@I)50K$Jeeb 1QW4l;U$+� S`Jf>nIOꊳ~R@"sp #';Dx# .vB=Z~P*$LT,sЄO0:GjM~TL5<w@&[tA6 n,[3�3M\¢r.X)&5"0kl:|ɷ[3P槱}V}y42Q8cvZ�&4s%zG. ӏL&&D]4dWk2dU8?0WAU0fuF^IG<<0oΗq8�΅e3Ҫ4 }eVy mbUl<J&M}w'B&CXb[X؄AL\s_Jx\kRۙ]>Bdq֐ �ovR<9?jb^]nW*L CDKX i^ }ȬO>@�>2GׁbY5*R$T $x E.b9TpdBib<]>%NټI!l+]W}v"9ӑGN�8rK{qb>hS{#jUOtL{lS^Q"FEc&c꘻ٳ燮g@| vTvfd~J=c<YYnO7ѓۤb1#ɖ#y`ޮ=;s  ̧?bT-xL9~.:3I>�gLB~F@%MV\ZqnN<K m;$\'Z&ŰLĊa>i:`H#4'ۭ;{�%HءrBCҲT`M(�!,Œ>RDhu�T5GX/UU2hx*0DQ+,Y5Te�8jB(St;Rр6܏2WڪӍlsT^xVUI, XNT IQ ed:�bL=QЋPWmRXR7{lyJCd۹vZW595c|5me4K;:8z(={8.'6A)$,7k[ cP/ <"-[g^YDbkmsOd팵|w&#3,g�RJl[K'+@E s"@˚yy 6AJؙ~� AAp!ҲUŋ  ݬzJ/~VpPa8YWE(� "RI!c15Pkb`bCSf̴#r aT )ws` -@#P8>88�j�,`3 '�h" eˮptHV{úY/՘E1yzY- [cXXT!:Гuf'XXKNx4hS? BK)D_]p'W7 NW˫=yD%J TN* 4CP-دZ a?7̎+*W R,XTvE-Dk}Nlo^#G;[%RkiܲGK-O!OWy pl27oL{zH*K<dM G ;Jrgܶe}UU*y}%_H|-[+<ԩ~P\O[Nؖ߼ơo-N>GnLDmkFVw##cy~Z.-Qn - CYFkupnx1lA3˶/ caEyS +ϟv=;&F^,W2⴨٪RiVo}_5OS^1d)eO�q[/'^,+YFz,�F/5DKQߍEYJ&P,D Q\7ᄓ1*9E^omw0/P[:\lc{Rn:7X*`�Xe+@6I&qnmlm#RrRn>_lkĉ^#7& +Y7�*ZZQE7U-Un*u=/֪ΝM So\Ugk)i0A-:DJijUasՓO4Sn1\`[;7lx:tj`g~m.8?{f>CtW)yruVŬoy8?xg<3|{�c;}k\5~zwlE } ``G8swvF)L�$j#$x.E˾syYRߙte[c6G]HVwƟ///�<:(8&W/�H_,gvv Ii>ɶ3�5֑6sWaWa̱Ocu<f]7,bZڻ,Bm2[mʯ]w\韙xmXvAYbzXykC4@VEogڶ=d}+^}/B� l=ċ@χ_gwwO߽s;X,icHvy?㓲@gܯ3u)�>où{ZmӦY8dѻ }<l|^rz7pxIh�zݻ}/R%w }UݕҨ5| @U[ۮ/?fx[M<v8W<[wq[N;&[w<pVb='�C+ @Vw�ۇ% V"pw�L,l.Uzks-6G e_HT#7c<Uoz3*9yښX0'u?0ʛ:i97ӛ8LZw 6-�ce' p|t;jM_eiѶr'MӆILYcm%�Wbcs ]bd 2;tA؊*0.5%P^keRA_(;@�uífKSA+G D]�$R2\*)#<550~ؓ\!8/)9c|S:~:Srm7M>|nݺV\!rr[֘l<Inf�@WU +aU^ EZng$o@Q)�CI_Έ+NBPR?X.!Q$5" %ȫצnlE$Uu_-lz N~ ǒIx*ke%[ӷC\2eC,bD횻]S;nuDW^0M[83|�ŇKќ5v<ȷM39YpN^ΔP>hc8IIJ'9A:OL)Q�Z ّJo<Ksz2)ǔ LB)}�k/'XT)j. T>]K"Y&|]$+ Vˋ(YWý冭`|<()QqģANITQ(/$uAӦ!>uxc\Dueߚ0wSB�RUݠR\9�p\+Rי$r.:ot^wON}>g.�vr9ӠCnϜ^Ⱦ92WHĒZJ*A<UOjÛtr32dq񪲪7W!�iUY>UE[?Lj!dqf-t;cpW}݌URw9Y@!ڣՏ;?mo(R*^ڪhV@s�e@Ƣ�l n=iR5E `q ҭ~ތMtm `S_n/yiVu|U AB\-:EE{P[|!4____\ʷYy2T\[=ߞ=~dϖ>]uB:G\l uyx¸OcY>SROeΘBe3Wk�o>R�IoYUeUfpitٽ1^mO9JDDDS;,#KDD10hQDD4Q(""(FM#&EDDň""bDD1hQDD4Q(""(FM#&EDDň""bDD1hQDD4Q(""(FM#&EDDň""bDD1hQDD4Q(""(FM#&EDDň""bDD1hQDD4Q(""(FM#&EDDň""bDD1hQDD4Q(""(FM#&EDDň""bDD1hQDD4Q(""(FMµ<G����IENDB`��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/extract.c�������������������������������������������������������������������������������0000664�0000000�0000000�00000015276�14766001452�0014400�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#include "extract.h" #include <string.h> #define LOG_MODULE "extract" #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" struct extraction_context { char32_t *buf; size_t size; size_t idx; size_t tab_spaces_left; size_t empty_count; size_t newline_count; bool strip_trailing_empty; bool failed; const struct row *last_row; const struct cell *last_cell; enum selection_kind selection_kind; }; struct extraction_context * extract_begin(enum selection_kind kind, bool strip_trailing_empty) { struct extraction_context *ctx = malloc(sizeof(*ctx)); if (unlikely(ctx == NULL)) { LOG_ERRNO("malloc() failed"); return NULL; } *ctx = (struct extraction_context){ .selection_kind = kind, .strip_trailing_empty = strip_trailing_empty, }; return ctx; } static bool ensure_size(struct extraction_context *ctx, size_t additional_chars) { while (ctx->size < ctx->idx + additional_chars) { size_t new_size = ctx->size == 0 ? 512 : ctx->size * 2; char32_t *new_buf = realloc(ctx->buf, new_size * sizeof(new_buf[0])); if (new_buf == NULL) return false; ctx->buf = new_buf; ctx->size = new_size; } xassert(ctx->size >= ctx->idx + additional_chars); return true; } bool extract_finish_wide(struct extraction_context *ctx, char32_t **text, size_t *len) { if (text == NULL) return false; *text = NULL; if (len != NULL) *len = 0; if (ctx->failed) goto err; if (!ctx->strip_trailing_empty) { /* Insert pending newlines, and replace empty cells with spaces */ if (!ensure_size(ctx, ctx->newline_count + ctx->empty_count)) goto err; for (size_t i = 0; i < ctx->newline_count; i++) ctx->buf[ctx->idx++] = U'\n'; for (size_t i = 0; i < ctx->empty_count; i++) ctx->buf[ctx->idx++] = U' '; } if (ctx->idx == 0) { /* Selection of empty cells only */ if (!ensure_size(ctx, 1)) goto err; ctx->buf[ctx->idx++] = U'\0'; } else { xassert(ctx->idx > 0); xassert(ctx->idx <= ctx->size); switch (ctx->selection_kind) { default: if (ctx->buf[ctx->idx - 1] == U'\n') ctx->buf[ctx->idx - 1] = U'\0'; break; case SELECTION_LINE_WISE: if (ctx->buf[ctx->idx - 1] != U'\n') { if (!ensure_size(ctx, 1)) goto err; ctx->buf[ctx->idx++] = U'\n'; } break; } if (ctx->buf[ctx->idx - 1] != U'\0') { if (!ensure_size(ctx, 1)) goto err; ctx->buf[ctx->idx++] = U'\0'; } } *text = ctx->buf; if (len != NULL) *len = ctx->idx - 1; free(ctx); return true; err: free(ctx->buf); free(ctx); return false; } bool extract_finish(struct extraction_context *ctx, char **text, size_t *len) { if (text == NULL) return false; if (len != NULL) *len = 0; char32_t *wtext; if (!extract_finish_wide(ctx, &wtext, NULL)) return false; bool ret = false; *text = ac32tombs(wtext); if (*text == NULL) { LOG_ERR("failed to convert selection to UTF-8"); goto out; } if (len != NULL) *len = strlen(*text); ret = true; out: free(wtext); return ret; } bool extract_one(const struct terminal *term, const struct row *row, const struct cell *cell, int col, void *context) { struct extraction_context *ctx = context; if (cell->wc >= CELL_SPACER) return true; if (ctx->last_row != NULL && row != ctx->last_row) { /* New row - determine if we should insert a newline or not */ if (ctx->selection_kind != SELECTION_BLOCK) { if (ctx->last_row->linebreak || ctx->empty_count > 0 || cell->wc == 0) { /* Row has a hard linebreak, or either last cell or * current cell is empty */ /* Don't emit newline just yet - only if there are * non-empty cells following it */ ctx->newline_count++; if (!ctx->strip_trailing_empty) { if (!ensure_size(ctx, ctx->empty_count)) goto err; for (size_t i = 0; i < ctx->empty_count; i++) ctx->buf[ctx->idx++] = U' '; } ctx->empty_count = 0; } } else { /* Always insert a linebreak */ if (!ensure_size(ctx, 1)) goto err; ctx->buf[ctx->idx++] = U'\n'; if (!ctx->strip_trailing_empty) { if (!ensure_size(ctx, ctx->empty_count)) goto err; for (size_t i = 0; i < ctx->empty_count; i++) ctx->buf[ctx->idx++] = U' '; } ctx->empty_count = 0; } ctx->tab_spaces_left = 0; } if (cell->wc == U' ' && ctx->tab_spaces_left > 0) { ctx->tab_spaces_left--; return true; } ctx->tab_spaces_left = 0; if (cell->wc == 0) { ctx->empty_count++; ctx->last_row = row; ctx->last_cell = cell; return true; } /* Insert pending newlines, and replace empty cells with spaces */ if (!ensure_size(ctx, ctx->newline_count + ctx->empty_count)) goto err; for (size_t i = 0; i < ctx->newline_count; i++) ctx->buf[ctx->idx++] = U'\n'; for (size_t i = 0; i < ctx->empty_count; i++) ctx->buf[ctx->idx++] = U' '; ctx->newline_count = 0; ctx->empty_count = 0; if (cell->wc >= CELL_COMB_CHARS_LO && cell->wc <= CELL_COMB_CHARS_HI) { const struct composed *composed = composed_lookup( term->composed, cell->wc - CELL_COMB_CHARS_LO); if (!ensure_size(ctx, composed->count)) goto err; for (size_t i = 0; i < composed->count; i++) ctx->buf[ctx->idx++] = composed->chars[i]; } else { if (!ensure_size(ctx, 1)) goto err; ctx->buf[ctx->idx++] = cell->wc; if (cell->wc == U'\t') { int next_tab_stop = term->cols - 1; tll_foreach(term->tab_stops, it) { if (it->item > col) { next_tab_stop = it->item; break; } } xassert(next_tab_stop >= col); ctx->tab_spaces_left = next_tab_stop - col; } } ctx->last_row = row; ctx->last_cell = cell; return true; err: ctx->failed = true; return false; } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/extract.h�������������������������������������������������������������������������������0000664�0000000�0000000�00000001035�14766001452�0014371�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#pragma once #include <stddef.h> #include <stdbool.h> #include <uchar.h> #include "terminal.h" struct extraction_context; struct extraction_context *extract_begin( enum selection_kind kind, bool strip_trailing_empty); bool extract_one( const struct terminal *term, const struct row *row, const struct cell *cell, int col, void *context); bool extract_finish( struct extraction_context *context, char **text, size_t *len); bool extract_finish_wide( struct extraction_context *context, char32_t **text, size_t *len); ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/fdm.c�����������������������������������������������������������������������������������0000664�0000000�0000000�00000026532�14766001452�0013471�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#include "fdm.h" #include <stdlib.h> #include <stdbool.h> #include <inttypes.h> #include <unistd.h> #include <errno.h> #include <fcntl.h> #include <signal.h> #include <sys/epoll.h> #include <tllist.h> #define LOG_MODULE "fdm" #define LOG_ENABLE_DBG 0 #include "log.h" #include "debug.h" #include "xmalloc.h" struct fd_handler { int fd; int events; fdm_fd_handler_t callback; void *callback_data; bool deleted; }; struct sig_handler { fdm_signal_handler_t callback; void *callback_data; }; struct hook { fdm_hook_t callback; void *callback_data; }; typedef tll(struct hook) hooks_t; struct fdm { int epoll_fd; bool is_polling; tll(struct fd_handler *) fds; tll(struct fd_handler *) deferred_delete; sigset_t sigmask; struct sig_handler *signal_handlers; hooks_t hooks_low; hooks_t hooks_normal; hooks_t hooks_high; }; static volatile sig_atomic_t got_signal = false; static volatile sig_atomic_t *received_signals = NULL; struct fdm * fdm_init(void) { sigset_t sigmask; if (sigprocmask(0, NULL, &sigmask) < 0) { LOG_ERRNO("failed to get process signal mask"); return NULL; } int epoll_fd = epoll_create1(EPOLL_CLOEXEC); if (epoll_fd == -1) { LOG_ERRNO("failed to create epoll FD"); return NULL; } xassert(received_signals == NULL); /* Only one FDM instance supported */ received_signals = xcalloc(SIGRTMAX, sizeof(received_signals[0])); got_signal = false; struct fdm *fdm = malloc(sizeof(*fdm)); if (unlikely(fdm == NULL)) { LOG_ERRNO("malloc() failed"); return NULL; } struct sig_handler *sig_handlers = calloc(SIGRTMAX, sizeof(sig_handlers[0])); if (sig_handlers == NULL) { LOG_ERRNO("failed to allocate signal handler array"); free(fdm); return NULL; } *fdm = (struct fdm){ .epoll_fd = epoll_fd, .is_polling = false, .fds = tll_init(), .deferred_delete = tll_init(), .sigmask = sigmask, .signal_handlers = sig_handlers, .hooks_low = tll_init(), .hooks_normal = tll_init(), .hooks_high = tll_init(), }; return fdm; } void fdm_destroy(struct fdm *fdm) { if (fdm == NULL) return; if (tll_length(fdm->fds) > 0) LOG_WARN("FD list not empty"); for (int i = 0; i < SIGRTMAX; i++) { if (fdm->signal_handlers[i].callback != NULL) LOG_WARN("handler for signal %d not removed", i); } if (tll_length(fdm->hooks_low) > 0 || tll_length(fdm->hooks_normal) > 0 || tll_length(fdm->hooks_high) > 0) { LOG_WARN("hook list not empty"); } xassert(tll_length(fdm->fds) == 0); xassert(tll_length(fdm->deferred_delete) == 0); xassert(tll_length(fdm->hooks_low) == 0); xassert(tll_length(fdm->hooks_normal) == 0); xassert(tll_length(fdm->hooks_high) == 0); sigprocmask(SIG_SETMASK, &fdm->sigmask, NULL); free(fdm->signal_handlers); tll_free(fdm->fds); tll_free(fdm->deferred_delete); tll_free(fdm->hooks_low); tll_free(fdm->hooks_normal); tll_free(fdm->hooks_high); close(fdm->epoll_fd); free(fdm); free((void *)received_signals); received_signals = NULL; } bool fdm_add(struct fdm *fdm, int fd, int events, fdm_fd_handler_t cb, void *data) { #if defined(_DEBUG) tll_foreach(fdm->fds, it) { if (it->item->fd == fd) { BUG("FD=%d already registered", fd); } } #endif struct fd_handler *handler = malloc(sizeof(*handler)); if (unlikely(handler == NULL)) { LOG_ERRNO("malloc() failed"); return false; } *handler = (struct fd_handler) { .fd = fd, .events = events, .callback = cb, .callback_data = data, .deleted = false, }; tll_push_back(fdm->fds, handler); struct epoll_event ev = { .events = events, .data = {.ptr = handler}, }; if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_ADD, fd, &ev) < 0) { LOG_ERRNO("failed to register FD=%d with epoll", fd); free(handler); tll_pop_back(fdm->fds); return false; } return true; } static bool fdm_del_internal(struct fdm *fdm, int fd, bool close_fd) { if (fd == -1) return true; tll_foreach(fdm->fds, it) { if (it->item->fd != fd) continue; if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_DEL, fd, NULL) < 0) LOG_ERRNO("failed to unregister FD=%d from epoll", fd); if (close_fd) close(it->item->fd); it->item->deleted = true; if (fdm->is_polling) tll_push_back(fdm->deferred_delete, it->item); else free(it->item); tll_remove(fdm->fds, it); return true; } LOG_ERR("no such FD: %d", fd); close(fd); return false; } bool fdm_del(struct fdm *fdm, int fd) { return fdm_del_internal(fdm, fd, true); } bool fdm_del_no_close(struct fdm *fdm, int fd) { return fdm_del_internal(fdm, fd, false); } static bool event_modify(struct fdm *fdm, struct fd_handler *fd, int new_events) { if (new_events == fd->events) return true; struct epoll_event ev = { .events = new_events, .data = {.ptr = fd}, }; if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_MOD, fd->fd, &ev) < 0) { LOG_ERRNO("failed to modify FD=%d with epoll (events 0x%08x -> 0x%08x)", fd->fd, fd->events, new_events); return false; } fd->events = new_events; return true; } bool fdm_event_add(struct fdm *fdm, int fd, int events) { tll_foreach(fdm->fds, it) { if (it->item->fd != fd) continue; return event_modify(fdm, it->item, it->item->events | events); } LOG_ERR("FD=%d not registered with the FDM", fd); return false; } bool fdm_event_del(struct fdm *fdm, int fd, int events) { tll_foreach(fdm->fds, it) { if (it->item->fd != fd) continue; return event_modify(fdm, it->item, it->item->events & ~events); } LOG_ERR("FD=%d not registered with the FDM", fd); return false; } static hooks_t * hook_priority_to_list(struct fdm *fdm, enum fdm_hook_priority priority) { switch (priority) { case FDM_HOOK_PRIORITY_LOW: return &fdm->hooks_low; case FDM_HOOK_PRIORITY_NORMAL: return &fdm->hooks_normal; case FDM_HOOK_PRIORITY_HIGH: return &fdm->hooks_high; } BUG("unhandled priority type"); return NULL; } bool fdm_hook_add(struct fdm *fdm, fdm_hook_t hook, void *data, enum fdm_hook_priority priority) { hooks_t *hooks = hook_priority_to_list(fdm, priority); #if defined(_DEBUG) tll_foreach(*hooks, it) { if (it->item.callback == hook) { LOG_ERR("hook=0x%" PRIxPTR " already registered", (uintptr_t)hook); return false; } } #endif tll_push_back(*hooks, ((struct hook){hook, data})); return true; } bool fdm_hook_del(struct fdm *fdm, fdm_hook_t hook, enum fdm_hook_priority priority) { hooks_t *hooks = hook_priority_to_list(fdm, priority); tll_foreach(*hooks, it) { if (it->item.callback != hook) continue; tll_remove(*hooks, it); return true; } LOG_WARN("hook=0x%" PRIxPTR " not registered", (uintptr_t)hook); return false; } static void signal_handler(int signo) { got_signal = true; received_signals[signo] = true; } bool fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *data) { if (fdm->signal_handlers[signo].callback != NULL) { LOG_ERR("signal %d already has a handler", signo); return false; } sigset_t mask, original; sigemptyset(&mask); sigaddset(&mask, signo); if (sigprocmask(SIG_BLOCK, &mask, &original) < 0) { LOG_ERRNO("failed to block signal %d", 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); sigprocmask(SIG_SETMASK, &original, NULL); return false; } received_signals[signo] = false; fdm->signal_handlers[signo].callback = handler; fdm->signal_handlers[signo].callback_data = data; return true; } bool fdm_signal_del(struct fdm *fdm, int signo) { if (fdm->signal_handlers[signo].callback == NULL) return false; struct sigaction action = {.sa_handler = SIG_DFL}; sigemptyset(&action.sa_mask); if (sigaction(signo, &action, NULL) < 0) { LOG_ERRNO("failed to restore signal handler for signal %d", signo); return false; } received_signals[signo] = false; fdm->signal_handlers[signo].callback = NULL; fdm->signal_handlers[signo].callback_data = NULL; sigset_t mask; sigemptyset(&mask); sigaddset(&mask, signo); if (sigprocmask(SIG_UNBLOCK, &mask, NULL) < 0) { LOG_ERRNO("failed to unblock signal %d", signo); return false; } return true; } bool fdm_poll(struct fdm *fdm) { xassert(!fdm->is_polling && "nested calls to fdm_poll() not allowed"); if (fdm->is_polling) { LOG_ERR("nested calls to fdm_poll() not allowed"); return false; } tll_foreach(fdm->hooks_high, it) { LOG_DBG( "executing high priority hook 0x%" PRIxPTR" (fdm=%p, data=%p)", (uintptr_t)it->item.callback, (void *)fdm, (void *)it->item.callback_data); it->item.callback(fdm, it->item.callback_data); } tll_foreach(fdm->hooks_normal, it) { LOG_DBG( "executing normal priority hook 0x%" PRIxPTR " (fdm=%p, data=%p)", (uintptr_t)it->item.callback, (void *)fdm, (void *)it->item.callback_data); it->item.callback(fdm, it->item.callback_data); } tll_foreach(fdm->hooks_low, it) { LOG_DBG( "executing low priority hook 0x%" PRIxPTR " (fdm=%p, data=%p)", (uintptr_t)it->item.callback, (void *)fdm, (void *)it->item.callback_data); it->item.callback(fdm, it->item.callback_data); } struct epoll_event events[tll_length(fdm->fds)]; int r = epoll_pwait( fdm->epoll_fd, events, tll_length(fdm->fds), -1, &fdm->sigmask); int errno_copy = errno; if (unlikely(got_signal)) { got_signal = false; for (int i = 0; i < SIGRTMAX; i++) { if (received_signals[i]) { received_signals[i] = false; struct sig_handler *handler = &fdm->signal_handlers[i]; xassert(handler->callback != NULL); if (!handler->callback(fdm, i, handler->callback_data)) return false; } } } if (unlikely(r < 0)) { if (errno_copy == EINTR) return true; LOG_ERRNO_P(errno_copy, "failed to epoll"); return false; } bool ret = true; fdm->is_polling = true; for (int i = 0; i < r; i++) { struct fd_handler *fd = events[i].data.ptr; if (fd->deleted) continue; if (!fd->callback(fdm, fd->fd, events[i].events, fd->callback_data)) { ret = false; break; } } fdm->is_polling = false; tll_foreach(fdm->deferred_delete, it) { free(it->item); tll_remove(fdm->deferred_delete, it); } return ret; } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/fdm.h�����������������������������������������������������������������������������������0000664�0000000�0000000�00000002131�14766001452�0013463�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#pragma once #include <stdbool.h> struct fdm; typedef bool (*fdm_fd_handler_t)(struct fdm *fdm, int fd, int events, void *data); typedef bool (*fdm_signal_handler_t)(struct fdm *fdm, int signo, void *data); typedef void (*fdm_hook_t)(struct fdm *fdm, void *data); enum fdm_hook_priority { FDM_HOOK_PRIORITY_LOW, FDM_HOOK_PRIORITY_NORMAL, FDM_HOOK_PRIORITY_HIGH }; struct fdm *fdm_init(void); void fdm_destroy(struct fdm *fdm); bool fdm_add(struct fdm *fdm, int fd, int events, fdm_fd_handler_t handler, void *data); bool fdm_del(struct fdm *fdm, int fd); bool fdm_del_no_close(struct fdm *fdm, int fd); bool fdm_event_add(struct fdm *fdm, int fd, int events); bool fdm_event_del(struct fdm *fdm, int fd, int events); bool fdm_hook_add(struct fdm *fdm, fdm_hook_t hook, void *data, enum fdm_hook_priority priority); bool fdm_hook_del(struct fdm *fdm, fdm_hook_t hook, enum fdm_hook_priority priority); bool fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *data); bool fdm_signal_del(struct fdm *fdm, int signo); bool fdm_poll(struct fdm *fdm); ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/foot-features.c�������������������������������������������������������������������������0000664�0000000�0000000�00000000741�14766001452�0015500�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#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(NDEBUG) " +assertions" #else " -assertions" #endif ; �������������������������������foot-1.21.0/foot-features.h�������������������������������������������������������������������������0000664�0000000�0000000�00000000360�14766001452�0015502�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#pragma once #include <stdio.h> extern const char version_and_features[]; static inline void print_version_and_features(const char *prefix) { fputs(prefix, stdout); fputs(version_and_features, stdout); fputc('\n', stdout); } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/foot-server.desktop���������������������������������������������������������������������0000664�0000000�0000000�00000000367�14766001452�0016423�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[Desktop Entry] Type=Application Exec=foot --server Icon=foot Terminal=false Categories=System;TerminalEmulator; Keywords=shell;prompt;command;commandline; Name=Foot Server GenericName=Terminal Comment=A wayland native terminal emulator (server) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/foot-server.service.in������������������������������������������������������������������0000664�0000000�0000000�00000000540�14766001452�0017010�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[Service] ExecStart=@bindir@/foot --server=3 UnsetEnvironment=LISTEN_PID LISTEN_FDS LISTEN_FDNAMES NonBlocking=true [Unit] Requires=%N.socket Description=Foot terminal server mode Documentation=man:foot(1) PartOf=graphical-session.target After=graphical-session.target ConditionEnvironment=WAYLAND_DISPLAY [Install] WantedBy=graphical-session.target ����������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/foot-server.socket����������������������������������������������������������������������0000664�0000000�0000000�00000000274�14766001452�0016237�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[Socket] ListenStream=%t/foot.sock [Unit] PartOf=graphical-session.target After=graphical-session.target ConditionEnvironment=WAYLAND_DISPLAY [Install] WantedBy=graphical-session.target ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/foot.desktop����������������������������������������������������������������������������0000664�0000000�0000000�00000000336�14766001452�0015113�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[Desktop Entry] Type=Application Exec=foot Icon=foot Terminal=false Categories=System;TerminalEmulator; Keywords=shell;prompt;command;commandline; Name=Foot GenericName=Terminal Comment=A wayland native terminal emulator ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/foot.info�������������������������������������������������������������������������������0000664�0000000�0000000�00000013374�14766001452�0014403�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������@default_terminfo@|foot terminal emulator, use=@default_terminfo@+base, colors#256, setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48\:5\:%p1%d%;m, setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38\:5\:%p1%d%;m, @default_terminfo@-direct|foot with direct color indexing, use=@default_terminfo@+base, colors#16777216, RGB, setab=\E[%?%p1%{8}%<%t4%p1%d%e48\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m, setaf=\E[%?%p1%{8}%<%t3%p1%d%e38\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m, @default_terminfo@+base|foot base fragment, AX, Su, Tc, XF, XT, am, bce, bw, ccc, hs, mir, msgr, npc, xenl, cols#80, it#8, lines#24, pairs#0x10000, BD=\E[?2004l, BE=\E[?2004h, Cr=\E]112\E\\, Cs=\E]12;%p1%s\E\\, E3=\E[3J, Ms=\E]52;%p1%s;%p2%s\E\\, PE=\E[201~, PS=\E[200~, RV=\E[>c, Rect=\E[%p1%d;%p2%d;%p3%d;%p4%d;%p5%d$x, Se=\E[ q, Setulc=\E[58\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m, Smulx=\E[4:%p1%dm, Ss=\E[%p1%d q, Sync=\E[?2026%?%p1%{1}%-%tl%eh%;, TS=\E]2;, XM=\E[?1006;1000%?%p1%{1}%=%th%el%;, XR=\E[>0q, acsc=``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, bel=^G, blink=\E[5m, bold=\E[1m, cbt=\E[Z, civis=\E[?25l, clear=\E[H\E[2J, cnorm=\E[?12l\E[?25h, cr=\r, csr=\E[%i%p1%d;%p2%dr, cub1=^H, cub=\E[%p1%dD, cud1=\n, cud=\E[%p1%dB, cuf1=\E[C, cuf=\E[%p1%dC, cup=\E[%i%p1%d;%p2%dH, cuu1=\E[A, cuu=\E[%p1%dA, cvvis=\E[?12;25h, dch1=\E[P, dch=\E[%p1%dP, dim=\E[2m, dl1=\E[M, dl=\E[%p1%dM, dsl=\E]2;\E\\, ech=\E[%p1%dX, ed=\E[J, el1=\E[1K, el=\E[K, fd=\E[?1004l, fe=\E[?1004h, flash=\E]555\E\\, fsl=\E\\, home=\E[H, hpa=\E[%i%p1%dG, ht=^I, hts=\EH, ich=\E[%p1%d@, il1=\E[L, il=\E[%p1%dL, ind=\n, indn=\E[%p1%dS, initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\, invis=\E[8m, is2=\E[!p\E[4l\E>, kDC3=\E[3;3~, kDC4=\E[3;4~, kDC5=\E[3;5~, kDC6=\E[3;6~, kDC7=\E[3;7~, kDC=\E[3;2~, kDN3=\E[1;3B, kDN4=\E[1;4B, kDN5=\E[1;5B, kDN6=\E[1;6B, kDN7=\E[1;7B, kDN=\E[1;2B, kEND3=\E[1;3F, kEND4=\E[1;4F, kEND5=\E[1;5F, kEND6=\E[1;6F, kEND7=\E[1;7F, kEND=\E[1;2F, kHOM3=\E[1;3H, kHOM4=\E[1;4H, kHOM5=\E[1;5H, kHOM6=\E[1;6H, kHOM7=\E[1;7H, kHOM=\E[1;2H, kIC3=\E[2;3~, kIC4=\E[2;4~, kIC5=\E[2;5~, kIC6=\E[2;6~, kIC7=\E[2;7~, kIC=\E[2;2~, kLFT3=\E[1;3D, kLFT4=\E[1;4D, kLFT5=\E[1;5D, kLFT6=\E[1;6D, kLFT7=\E[1;7D, kLFT=\E[1;2D, kNXT3=\E[6;3~, kNXT4=\E[6;4~, kNXT5=\E[6;5~, kNXT6=\E[6;6~, kNXT7=\E[6;7~, kNXT=\E[6;2~, kPRV3=\E[5;3~, kPRV4=\E[5;4~, kPRV5=\E[5;5~, kPRV6=\E[5;6~, kPRV7=\E[5;7~, kPRV=\E[5;2~, kRIT3=\E[1;3C, kRIT4=\E[1;4C, kRIT5=\E[1;5C, kRIT6=\E[1;6C, kRIT7=\E[1;7C, kRIT=\E[1;2C, kUP3=\E[1;3A, kUP4=\E[1;4A, kUP5=\E[1;5A, kUP6=\E[1;6A, kUP7=\E[1;7A, kUP=\E[1;2A, kbs=^?, kcbt=\E[Z, kcub1=\EOD, kcud1=\EOB, kcuf1=\EOC, kcuu1=\EOA, kdch1=\E[3~, kend=\EOF, kf10=\E[21~, kf11=\E[23~, kf12=\E[24~, kf13=\E[1;2P, kf14=\E[1;2Q, kf15=\E[1;2R, kf16=\E[1;2S, kf17=\E[15;2~, kf18=\E[17;2~, kf19=\E[18;2~, kf1=\EOP, kf20=\E[19;2~, kf21=\E[20;2~, kf22=\E[21;2~, kf23=\E[23;2~, kf24=\E[24;2~, kf25=\E[1;5P, kf26=\E[1;5Q, kf27=\E[1;5R, kf28=\E[1;5S, kf29=\E[15;5~, kf2=\EOQ, kf30=\E[17;5~, kf31=\E[18;5~, kf32=\E[19;5~, kf33=\E[20;5~, kf34=\E[21;5~, kf35=\E[23;5~, kf36=\E[24;5~, kf37=\E[1;6P, kf38=\E[1;6Q, kf39=\E[1;6R, kf3=\EOR, kf40=\E[1;6S, kf41=\E[15;6~, kf42=\E[17;6~, kf43=\E[18;6~, kf44=\E[19;6~, kf45=\E[20;6~, kf46=\E[21;6~, kf47=\E[23;6~, kf48=\E[24;6~, kf49=\E[1;3P, kf4=\EOS, kf50=\E[1;3Q, kf51=\E[1;3R, kf52=\E[1;3S, kf53=\E[15;3~, kf54=\E[17;3~, kf55=\E[18;3~, kf56=\E[19;3~, kf57=\E[20;3~, kf58=\E[21;3~, kf59=\E[23;3~, kf5=\E[15~, kf60=\E[24;3~, kf61=\E[1;4P, kf62=\E[1;4Q, kf63=\E[1;4R, kf6=\E[17~, kf7=\E[18~, kf8=\E[19~, kf9=\E[20~, khome=\EOH, kich1=\E[2~, kind=\E[1;2B, kmous=\E[<, knp=\E[6~, kpp=\E[5~, kri=\E[1;2A, kxIN=\E[I, kxOUT=\E[O, nel=\EE, oc=\E]104\E\\, op=\E[39;49m, rc=\E8, rep=%p1%c\E[%p2%{1}%-%db, rev=\E[7m, ri=\EM, rin=\E[%p1%dT, ritm=\E[23m, rmacs=\E(B, rmam=\E[?7l, rmcup=\E[?1049l\E[23;0;0t, rmir=\E[4l, rmkx=\E[?1l\E>, rmm=\E[?1036h\E[?1034l, rmso=\E[27m, rmul=\E[24m, rmxx=\E[29m, rs1=\Ec, rs2=\E[!p\E[4l\E>, rv=\E\\[>1;[0-9][0-9][0-9][0-9][0-9][0-9];0c, sc=\E7, setal=\E[58\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m, setrgbb=\E[48\:2\:\:%p1%d\:%p2%d\:%p3%dm, setrgbf=\E[38\:2\:\:%p1%d\:%p2%d\:%p3%dm, sgr0=\E(B\E[m, sgr=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p5%t;2%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m, sitm=\E[3m, smacs=\E(0, smam=\E[?7h, smcup=\E[?1049h\E[22;0;0t, smir=\E[4h, smkx=\E[?1h\E=, smm=\E[?1036l\E[?1034h, smso=\E[7m, smul=\E[4m, smxx=\E[9m, tbc=\E[3g, tsl=\E]2;, u6=\E[%i%d;%dR, u7=\E[6n, u8=\E[?%[;0123456789]c, u9=\E[c, vpa=\E[%i%p1%dd, xm=\E[<%i%p3%d;%p1%d;%p2%d;%?%p4%tM%em%;, xr=\EP>\\|foot\\([0-9]+\\.[0-9]+\\.[0-9]+(-[0-9]+-g[a-f[0-9]+)?\\)?\E\\\\, # XT, # AX, ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/foot.ini��������������������������������������������������������������������������������0000664�0000000�0000000�00000017707�14766001452�0014233�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- conf -*- # shell=$SHELL (if set, otherwise user's default shell from /etc/passwd) # term=foot (or xterm-256color if built with -Dterminfo=disabled) # login-shell=no # app-id=foot # globally set wayland app-id. Default values are "foot" and "footclient" for desktop and server mode # title=foot # locked-title=no # font=monospace:size=8 # font-bold=<bold variant of regular font> # font-italic=<italic variant of regular font> # font-bold-italic=<bold+italic variant of regular font> # font-size-adjustment=0.5 # line-height=<font metrics> # letter-spacing=0 # horizontal-letter-offset=0 # vertical-letter-offset=0 # underline-offset=<font metrics> # underline-thickness=<font underline thickness> # strikeout-thickness=<font strikeout thickness> # box-drawings-uses-font-glyphs=no # dpi-aware=no # initial-window-size-pixels=700x500 # Or, # initial-window-size-chars=<COLSxROWS> # initial-window-mode=windowed # pad=0x0 # optionally append 'center' # resize-by-cells=yes # resize-keep-grid=yes # resize-delay-ms=100 # bold-text-in-bright=no # word-delimiters=,│`|:"'()[]{}<> # selection-target=primary # workers=<number of logical CPUs> # utmp-helper=/usr/lib/utempter/utempter # When utmp backend is ‘libutempter’ (Linux) # utmp-helper=/usr/libexec/ulog-helper # When utmp backend is ‘ulog’ (FreeBSD) [environment] # name=value [security] # osc52=enabled # disabled|copy-enabled|paste-enabled|enabled [bell] # system=yes # urgent=no # notify=no # visual=no # command= # command-focused=no [desktop-notifications] # command=notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --hint BOOLEAN:suppress-sound:${muted} --hint STRING:sound-name:${sound-name} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body} # command-action-argument=--action ${action-name}=${action-label} # close="" # inhibit-when-focused=yes [scrollback] # lines=1000 # multiplier=3.0 # indicator-position=relative # indicator-format="" [url] # launch=xdg-open ${url} # label-letters=sadfjklewcmpgh # osc8-underline=url-mode # regex=(([a-z][[:alnum:]-]+:(/{1,3}|[a-z0-9%])|www[:digit:]{0,3}[.])([^[:space:](){}<>]+|\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\])+(\(([^[:space:](){}<>]+|(\([^[:space:](){}<>]+\)))*\)|\[([^]\[[:space:](){}<>]+|(\[[^]\[[:space:](){}<>]+\]))*\]|[^]\[[:space:]`!(){};:'".,<>?«»“”‘’])) # 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> [mouse] # hide-when-typing=no # alternate-scroll-mode=yes [touch] # long-press-delay=400 [colors] # alpha=1.0 # background=242424 # foreground=ffffff # flash=7f7f00 # flash-alpha=0.5 ## Normal/regular colors (color palette 0-7) # regular0=242424 # black # regular1=f62b5a # red # regular2=47b413 # green # regular3=e3c401 # yellow # regular4=24acd4 # blue # regular5=f2affd # magenta # regular6=13c299 # cyan # regular7=e6e6e6 # white ## Bright colors (color palette 8-15) # bright0=616161 # bright black # bright1=ff4d51 # bright red # bright2=35d450 # bright green # bright3=e9e836 # bright yellow # bright4=5dc5f8 # bright blue # bright5=feabf2 # bright magenta # bright6=24dfc4 # bright cyan # bright7=ffffff # bright white ## dimmed colors (see foot.ini(5) man page) # dim0=<not set> # ... # dim7=<not-set> ## The remaining 256-color palette # 16 = <256-color palette #16> # ... # 255 = <256-color palette #255> ## Sixel colors # sixel0 = 000000 # sixel1 = 3333cc # sixel2 = cc2121 # sixel3 = 33cc33 # sixel4 = cc33cc # sixel5 = 33cccc # sixel6 = cccc33 # sixel7 = 878787 # sixel8 = 424242 # sixel9 = 545499 # sixel10 = 994242 # sixel11 = 549954 # sixel12 = 995499 # sixel13 = 549999 # sixel14 = 999954 # sixel15 = cccccc ## Misc colors # selection-foreground=<inverse foreground/background> # selection-background=<inverse foreground/background> # jump-labels=<regular0> <regular3> # black-on-yellow # scrollback-indicator=<regular0> <bright4> # black-on-bright-blue # search-box-no-match=<regular0> <regular1> # black-on-red # search-box-match=<regular0> <regular3> # black-on-yellow # urls=<regular3> [csd] # preferred=server # size=26 # font=<primary font> # color=<foreground color> # hide-when-maximized=no # double-click-to-maximize=yes # border-width=0 # border-color=<csd.color> # button-width=26 # button-color=<background color> # button-minimize-color=<regular4> # button-maximize-color=<regular2> # button-close-color=<regular1> [key-bindings] # scrollback-up-page=Shift+Page_Up Shift+KP_Page_Up # scrollback-up-half-page=none # scrollback-up-line=none # scrollback-down-page=Shift+Page_Down Shift+KP_Page_Down # scrollback-down-half-page=none # scrollback-down-line=none # scrollback-home=none # scrollback-end=none # clipboard-copy=Control+Shift+c XF86Copy # clipboard-paste=Control+Shift+v XF86Paste # primary-paste=Shift+Insert # search-start=Control+Shift+r # font-increase=Control+plus Control+equal Control+KP_Add # font-decrease=Control+minus Control+KP_Subtract # font-reset=Control+0 Control+KP_0 # spawn-terminal=Control+Shift+n # minimize=none # maximize=none # fullscreen=none # pipe-visible=[sh -c "xurls | fuzzel | xargs -r firefox"] none # pipe-scrollback=[sh -c "xurls | fuzzel | xargs -r firefox"] none # pipe-selected=[xargs -r firefox] none # pipe-command-output=[wl-copy] none # Copy last command's output to the clipboard # show-urls-launch=Control+Shift+o # show-urls-copy=none # show-urls-persistent=none # prompt-prev=Control+Shift+z # prompt-next=Control+Shift+x # unicode-input=Control+Shift+u # noop=none # quit=none [search-bindings] # cancel=Control+g Control+c Escape # commit=Return KP_Enter # find-prev=Control+r # find-next=Control+s # cursor-left=Left Control+b # cursor-left-word=Control+Left Mod1+b # cursor-right=Right Control+f # cursor-right-word=Control+Right Mod1+f # cursor-home=Home Control+a # cursor-end=End Control+e # delete-prev=BackSpace # delete-prev-word=Mod1+BackSpace Control+BackSpace # delete-next=Delete # delete-next-word=Mod1+d Control+Delete # delete-to-start=Control+u # delete-to-end=Control+k # extend-char=Shift+Right # extend-to-word-boundary=Control+w Control+Shift+Right # extend-to-next-whitespace=Control+Shift+w # extend-line-down=Shift+Down # extend-backward-char=Shift+Left # extend-backward-to-word-boundary=Control+Shift+Left # extend-backward-to-next-whitespace=none # extend-line-up=Shift+Up # clipboard-paste=Control+v Control+Shift+v Control+y XF86Paste # primary-paste=Shift+Insert # unicode-input=none # scrollback-up-page=Shift+Page_Up Shift+KP_Page_Up # scrollback-up-half-page=none # scrollback-up-line=none # scrollback-down-page=Shift+Page_Down Shift+KP_Page_Down # scrollback-down-half-page=none # scrollback-down-line=none # scrollback-home=none # scrollback-end=none [url-bindings] # cancel=Control+g Control+c Control+d Escape # toggle-url-visible=t [text-bindings] # \x03=Mod4+c # Map Super+c -> Ctrl+c [mouse-bindings] # scrollback-up-mouse=BTN_WHEEL_BACK # scrollback-down-mouse=BTN_WHEEL_FORWARD # font-increase=Control+BTN_WHEEL_BACK # font-decrease=Control+BTN_WHEEL_FORWARD # selection-override-modifiers=Shift # primary-paste=BTN_MIDDLE # select-begin=BTN_LEFT # select-begin-block=Control+BTN_LEFT # select-extend=BTN_RIGHT # select-extend-character-wise=Control+BTN_RIGHT # select-word=BTN_LEFT-2 # select-word-whitespace=Control+BTN_LEFT-2 # select-quote = BTN_LEFT-3 # select-row=BTN_LEFT-4 # vim: ft=dosini ���������������������������������������������������������foot-1.21.0/footclient.desktop����������������������������������������������������������������������0000664�0000000�0000000�00000000364�14766001452�0016313�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[Desktop Entry] Type=Application Exec=footclient Icon=foot Terminal=false Categories=System;TerminalEmulator; Keywords=shell;prompt;command;commandline; Name=Foot Client GenericName=Terminal Comment=A wayland native terminal emulator (client) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/generate-version.sh���������������������������������������������������������������������0000775�0000000�0000000�00000003101�14766001452�0016356�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh set -e if [ ${#} -ne 3 ]; then echo "Usage: ${0} <default_version> <src_dir> <out_file>" exit 1 fi default_version=${1} src_dir=${2} out_file=${3} # echo "default version: ${default_version}" # echo "source directory: ${src_dir}" # echo "output file: ${out_file}" if [ -d "${src_dir}/.git" ] && command -v git > /dev/null; then workdir=$(pwd) cd "${src_dir}" if git describe --tags > /dev/null 2>&1; then git_version=$(git describe --always --tags) else # No tags available, happens in e.g. CI builds git_version="${default_version}" fi git_branch=$(git rev-parse --abbrev-ref HEAD) cd "${workdir}" new_version="${git_version} ($(date "+%b %d %Y"), branch '${git_branch}')" else new_version="${default_version}" extra="" fi major=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\1/') minor=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\2/') patch=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\3/') extra=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9]+-g[a-z0-9]+) .*)?.*/\5/') new_version="#define FOOT_VERSION \"${new_version}\" #define FOOT_MAJOR ${major} #define FOOT_MINOR ${minor} #define FOOT_PATCH ${patch} #define FOOT_EXTRA \"${extra}\"" if [ -f "${out_file}" ]; then old_version=$(cat "${out_file}") else old_version="" fi # echo "old version: ${old_version}" # echo "new version: ${new_version}" if [ "${old_version}" != "${new_version}" ]; then echo "${new_version}" > "${out_file}" fi ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/grid.c����������������������������������������������������������������������������������0000664�0000000�0000000�00000153743�14766001452�0013655�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#include "grid.h" #include <limits.h> #include <stdlib.h> #include <string.h> #define LOG_MODULE "grid" #define LOG_ENABLE_DBG 0 #include "log.h" #include "debug.h" #include "macros.h" #include "sixel.h" #include "stride.h" #include "util.h" #include "xmalloc.h" #define TIME_REFLOW 0 #if defined(TIME_REFLOW) #include "misc.h" #endif /* * "sb" (scrollback relative) coordinates * * The scrollback relative row number 0 is the *first*, and *oldest* * row in the scrollback history (and thus the *first* row to be * scrolled out). Thus, a higher number means further *down* in the * scrollback, with the *highest* number being at the bottom of the * screen, where new input appears. */ int grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row) { const int scrollback_start = grid->offset + screen_rows; int rebased_row = abs_row - scrollback_start + grid->num_rows; rebased_row &= grid->num_rows - 1; return rebased_row; } int grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row) { const int scrollback_start = grid->offset + screen_rows; int abs_row = sb_rel_row + scrollback_start; abs_row &= grid->num_rows - 1; return abs_row; } int grid_sb_start_ignore_uninitialized(const struct grid *grid, int screen_rows) { int scrollback_start = grid->offset + screen_rows; scrollback_start &= grid->num_rows - 1; while (grid->rows[scrollback_start] == NULL) { scrollback_start++; scrollback_start &= grid->num_rows - 1; } return scrollback_start; } int grid_row_abs_to_sb_precalc_sb_start(const struct grid *grid, int sb_start, int abs_row) { int rebased_row = abs_row - sb_start + grid->num_rows; rebased_row &= grid->num_rows - 1; return rebased_row; } int grid_row_sb_to_abs_precalc_sb_start(const struct grid *grid, int sb_start, int sb_rel_row) { int abs_row = sb_rel_row + sb_start; abs_row &= grid->num_rows - 1; return abs_row; } static void ensure_row_has_extra_data(struct row *row) { if (row->extra == NULL) row->extra = xcalloc(1, sizeof(*row->extra)); } static void verify_no_overlapping_ranges_of_type(const struct row_ranges *ranges, enum row_range_type type) { #if defined(_DEBUG) for (size_t i = 0; i < ranges->count; i++) { const struct row_range *r1 = &ranges->v[i]; for (size_t j = i + 1; j < ranges->count; j++) { const struct row_range *r2 = &ranges->v[j]; xassert(r1 != r2); if ((r1->start <= r2->start && r1->end >= r2->start) || (r1->start <= r2->end && r1->end >= r2->end)) { switch (type) { case ROW_RANGE_URI: BUG("OSC-8 URI overlap: %s: %d-%d: %s: %d-%d", r1->uri.uri, r1->start, r1->end, r2->uri.uri, r2->start, r2->end); break; case ROW_RANGE_UNDERLINE: BUG("underline overlap: %d-%d, %d-%d", r1->start, r1->end, r2->start, r2->end); break; } } } } #endif } static void verify_no_overlapping_ranges(const struct row_data *extra) { verify_no_overlapping_ranges_of_type(&extra->uri_ranges, ROW_RANGE_URI); verify_no_overlapping_ranges_of_type(&extra->underline_ranges, ROW_RANGE_UNDERLINE); } static void verify_ranges_of_type_are_sorted(const struct row_ranges *ranges, enum row_range_type type) { #if defined(_DEBUG) const struct row_range *last = NULL; for (size_t i = 0; i < ranges->count; i++) { const struct row_range *r = &ranges->v[i]; if (last != NULL) { if (last->start >= r->start || last->end >= r->end) { switch (type) { case ROW_RANGE_URI: BUG("OSC-8 URI not sorted correctly: " "%s: %d-%d came before %s: %d-%d", last->uri.uri, last->start, last->end, r->uri.uri, r->start, r->end); break; case ROW_RANGE_UNDERLINE: BUG("underline ranges not sorted correctly: " "%d-%d came before %d-%d", last->start, last->end, r->start, r->end); break; } } } last = r; } #endif } static void verify_ranges_are_sorted(const struct row_data *extra) { verify_ranges_of_type_are_sorted(&extra->uri_ranges, ROW_RANGE_URI); verify_ranges_of_type_are_sorted(&extra->underline_ranges, ROW_RANGE_UNDERLINE); } static void range_ensure_size(struct row_ranges *ranges, int count_to_add) { if (ranges->count + count_to_add > ranges->size) { ranges->size = ranges->count + count_to_add; ranges->v = xrealloc(ranges->v, ranges->size * sizeof(ranges->v[0])); } xassert(ranges->count + count_to_add <= ranges->size); } /* * Be careful! This function may xrealloc() the URI range vector, thus * invalidating pointers into it. */ static void range_insert(struct row_ranges *ranges, size_t idx, int start, int end, enum row_range_type type, const union row_range_data *data) { range_ensure_size(ranges, 1); xassert(idx <= ranges->count); const size_t move_count = ranges->count - idx; memmove(&ranges->v[idx + 1], &ranges->v[idx], move_count * sizeof(ranges->v[0])); ranges->count++; struct row_range *r = &ranges->v[idx]; r->start = start; r->end = end; switch (type) { case ROW_RANGE_URI: r->uri.id = data->uri.id; r->uri.uri = xstrdup(data->uri.uri); break; case ROW_RANGE_UNDERLINE: r->underline = data->underline; break; } } static void range_append_by_ref(struct row_ranges *ranges, int start, int end, enum row_range_type type, const union row_range_data *data) { range_ensure_size(ranges, 1); struct row_range *r = &ranges->v[ranges->count++]; r->start = start; r->end = end; switch (type) { case ROW_RANGE_URI: r->uri.id = data->uri.id;; r->uri.uri = data->uri.uri; break; case ROW_RANGE_UNDERLINE: r->underline = data->underline; break; } } static void range_append(struct row_ranges *ranges, int start, int end, enum row_range_type type, const union row_range_data *data) { switch (type) { case ROW_RANGE_URI: range_append_by_ref( ranges, start, end, type, &(union row_range_data){.uri = {.id = data->uri.id, .uri = xstrdup(data->uri.uri)}}); break; case ROW_RANGE_UNDERLINE: range_append_by_ref(ranges, start, end, type, data); break; } } static void range_delete(struct row_ranges *ranges, enum row_range_type type, size_t idx) { xassert(idx < ranges->count); grid_row_range_destroy(&ranges->v[idx], type); const size_t move_count = ranges->count - idx - 1; memmove(&ranges->v[idx], &ranges->v[idx + 1], move_count * sizeof(ranges->v[0])); ranges->count--; } struct grid * grid_snapshot(const struct grid *grid) { struct grid *clone = xmalloc(sizeof(*clone)); clone->num_rows = grid->num_rows; clone->num_cols = grid->num_cols; clone->offset = grid->offset; clone->view = grid->view; clone->cursor = grid->cursor; clone->saved_cursor = grid->saved_cursor; clone->kitty_kbd = grid->kitty_kbd; clone->rows = xcalloc(grid->num_rows, sizeof(clone->rows[0])); memset(&clone->scroll_damage, 0, sizeof(clone->scroll_damage)); memset(&clone->sixel_images, 0, sizeof(clone->sixel_images)); tll_foreach(grid->scroll_damage, it) tll_push_back(clone->scroll_damage, it->item); for (int r = 0; r < grid->num_rows; r++) { const struct row *row = grid->rows[r]; if (row == NULL) continue; struct row *clone_row = xmalloc(sizeof(*row)); clone->rows[r] = clone_row; clone_row->cells = xmalloc(grid->num_cols * sizeof(clone_row->cells[0])); clone_row->linebreak = row->linebreak; clone_row->dirty = row->dirty; clone_row->shell_integration = row->shell_integration; for (int c = 0; c < grid->num_cols; c++) clone_row->cells[c] = row->cells[c]; const struct row_data *extra = row->extra; if (extra != NULL) { struct row_data *clone_extra = xcalloc(1, sizeof(*clone_extra)); clone_row->extra = clone_extra; range_ensure_size(&clone_extra->uri_ranges, extra->uri_ranges.count); range_ensure_size(&clone_extra->underline_ranges, extra->underline_ranges.count); for (int i = 0; i < extra->uri_ranges.count; i++) { const struct row_range *range = &extra->uri_ranges.v[i]; range_append( &clone_extra->uri_ranges, range->start, range->end, ROW_RANGE_URI, &range->data); } for (int i = 0; i < extra->underline_ranges.count; i++) { const struct row_range *range = &extra->underline_ranges.v[i]; range_append_by_ref( &clone_extra->underline_ranges, range->start, range->end, ROW_RANGE_UNDERLINE, &range->data); } } else clone_row->extra = NULL; } tll_foreach(grid->sixel_images, it) { int original_width = it->item.original.width; int original_height = it->item.original.height; pixman_image_t *original_pix = it->item.original.pix; pixman_format_code_t original_pix_fmt = pixman_image_get_format(original_pix); int original_stride = stride_for_format_and_width(original_pix_fmt, original_width); size_t original_size = original_stride * original_height; void *new_original_data = xmemdup(it->item.original.data, original_size); pixman_image_t *new_original_pix = pixman_image_create_bits_no_clear( original_pix_fmt, original_width, original_height, new_original_data, original_stride); void *new_scaled_data = NULL; pixman_image_t *new_scaled_pix = NULL; int scaled_width = -1; int scaled_height = -1; if (it->item.scaled.data != NULL) { scaled_width = it->item.scaled.width; scaled_height = it->item.scaled.height; pixman_image_t *scaled_pix = it->item.scaled.pix; pixman_format_code_t scaled_pix_fmt = pixman_image_get_format(scaled_pix); int scaled_stride = stride_for_format_and_width(scaled_pix_fmt, scaled_width); size_t scaled_size = scaled_stride * scaled_height; new_scaled_data = xmemdup(it->item.scaled.data, scaled_size); new_scaled_pix = pixman_image_create_bits_no_clear( scaled_pix_fmt, scaled_width, scaled_height, new_scaled_data, scaled_stride); } struct sixel six = { .pix = (it->item.pix == it->item.original.pix ? new_original_pix : (it->item.pix == it->item.scaled.pix ? new_scaled_pix : NULL)), .width = it->item.width, .height = it->item.height, .rows = it->item.rows, .cols = it->item.cols, .pos = it->item.pos, .opaque = it->item.opaque, .cell_width = it->item.cell_width, .cell_height = it->item.cell_height, .original = { .data = new_original_data, .pix = new_original_pix, .width = original_width, .height = original_height, }, .scaled = { .data = new_scaled_data, .pix = new_scaled_pix, .width = scaled_width, .height = scaled_height, }, }; tll_push_back(clone->sixel_images, six); } return clone; } void grid_free(struct grid *grid) { if (grid == NULL) return; for (int r = 0; r < grid->num_rows; r++) grid_row_free(grid->rows[r]); tll_foreach(grid->sixel_images, it) { sixel_destroy(&it->item); tll_remove(grid->sixel_images, it); } free(grid->rows); tll_free(grid->scroll_damage); } void grid_swap_row(struct grid *grid, int row_a, int row_b) { xassert(grid->offset >= 0); xassert(row_a != row_b); int real_a = (grid->offset + row_a) & (grid->num_rows - 1); int real_b = (grid->offset + row_b) & (grid->num_rows - 1); struct row *a = grid->rows[real_a]; struct row *b = grid->rows[real_b]; grid->rows[real_a] = b; grid->rows[real_b] = a; } struct row * grid_row_alloc(int cols, bool initialize) { struct row *row = xmalloc(sizeof(*row)); row->dirty = false; row->linebreak = true; row->extra = NULL; row->shell_integration.prompt_marker = false; row->shell_integration.cmd_start = -1; row->shell_integration.cmd_end = -1; if (initialize) { row->cells = xcalloc(cols, sizeof(row->cells[0])); for (size_t c = 0; c < cols; c++) row->cells[c].attrs.clean = 1; } else row->cells = xmalloc(cols * sizeof(row->cells[0])); return row; } void grid_row_free(struct row *row) { if (row == NULL) return; grid_row_reset_extra(row); free(row->extra); free(row->cells); free(row); } void grid_resize_without_reflow( struct grid *grid, int new_rows, int new_cols, int old_screen_rows, int new_screen_rows) { struct row *const *old_grid = grid->rows; const int old_rows = grid->num_rows; const int old_cols = grid->num_cols; struct row **new_grid = xcalloc(new_rows, sizeof(new_grid[0])); tll(struct sixel) untranslated_sixels = tll_init(); tll_foreach(grid->sixel_images, it) tll_push_back(untranslated_sixels, it->item); tll_free(grid->sixel_images); int new_offset = 0; /* Copy old lines, truncating them if old rows were longer */ for (int r = 0, n = min(old_screen_rows, new_screen_rows); r < n; r++) { const int old_row_idx = (grid->offset + r) & (old_rows - 1); const int new_row_idx = (new_offset + r) & (new_rows - 1); const struct row *old_row = old_grid[old_row_idx]; xassert(old_row != NULL); struct row *new_row = grid_row_alloc(new_cols, false); new_grid[new_row_idx] = new_row; memcpy(new_row->cells, old_row->cells, sizeof(struct cell) * min(old_cols, new_cols)); new_row->dirty = old_row->dirty; new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker; new_row->shell_integration.cmd_start = min(old_row->shell_integration.cmd_start, new_cols - 1); new_row->shell_integration.cmd_end = min(old_row->shell_integration.cmd_end, new_cols - 1); if (new_cols > old_cols) { /* Clear "new" columns */ memset(&new_row->cells[old_cols], 0, sizeof(struct cell) * (new_cols - old_cols)); new_row->dirty = true; } else if (old_cols > new_cols) { /* Make sure we don't cut a multi-column character in two */ for (int i = new_cols; i > 0 && old_row->cells[i].wc > CELL_SPACER; i--) new_row->cells[i - 1].wc = 0; } /* Map sixels on current "old" row to current "new row" */ tll_foreach(untranslated_sixels, it) { if (it->item.pos.row != old_row_idx) continue; struct sixel sixel = it->item; sixel.pos.row = new_row_idx; if (sixel.pos.col < new_cols) tll_push_back(grid->sixel_images, sixel); else sixel_destroy(&it->item); tll_remove(untranslated_sixels, it); } /* Copy URI ranges, truncating them if necessary */ const struct row_data *old_extra = old_row->extra; if (old_extra == NULL) continue; ensure_row_has_extra_data(new_row); struct row_data *new_extra = new_row->extra; range_ensure_size(&new_extra->uri_ranges, old_extra->uri_ranges.count); range_ensure_size(&new_extra->underline_ranges, old_extra->underline_ranges.count); for (int i = 0; i < old_extra->uri_ranges.count; i++) { const struct row_range *range = &old_extra->uri_ranges.v[i]; if (range->start >= new_cols) { /* The whole range is truncated */ continue; } const int start = range->start; const int end = min(range->end, new_cols - 1); range_append(&new_extra->uri_ranges, start, end, ROW_RANGE_URI, &range->data); } for (int i = 0; i < old_extra->underline_ranges.count; i++) { const struct row_range *range = &old_extra->underline_ranges.v[i]; if (range->start >= new_cols) { /* The whole range is truncated */ continue; } const int start = range->start; const int end = min(range->end, new_cols - 1); range_append_by_ref(&new_extra->underline_ranges, start, end, ROW_RANGE_UNDERLINE, &range->data); } } /* Clear "new" lines */ for (int r = min(old_screen_rows, new_screen_rows); r < new_screen_rows; r++) { struct row *new_row = grid_row_alloc(new_cols, false); new_grid[(new_offset + r) & (new_rows - 1)] = new_row; memset(new_row->cells, 0, sizeof(struct cell) * new_cols); new_row->dirty = true; } #if defined(_DEBUG) for (size_t r = 0; r < new_rows; r++) { const struct row *row = new_grid[r]; if (row == NULL) continue; if (row->extra == NULL) continue; verify_no_overlapping_ranges(row->extra); verify_ranges_are_sorted(row->extra); } #endif /* Free old grid */ for (int r = 0; r < grid->num_rows; r++) grid_row_free(old_grid[r]); free(grid->rows); grid->rows = new_grid; grid->num_rows = new_rows; grid->num_cols = new_cols; grid->view = grid->offset = new_offset; /* Keep cursor at current position, but clamp to new dimensions */ struct coord cursor = grid->cursor.point; if (cursor.row == old_screen_rows - 1) { /* 'less' breaks if the cursor isn't at the bottom */ cursor.row = new_screen_rows - 1; } cursor.row = min(cursor.row, new_screen_rows - 1); cursor.col = min(cursor.col, new_cols - 1); grid->cursor.point = cursor; struct coord saved_cursor = grid->saved_cursor.point; if (saved_cursor.row == old_screen_rows - 1) saved_cursor.row = new_screen_rows - 1; saved_cursor.row = min(saved_cursor.row, new_screen_rows - 1); saved_cursor.col = min(saved_cursor.col, new_cols - 1); grid->saved_cursor.point = saved_cursor; grid->cur_row = new_grid[(grid->offset + cursor.row) & (new_rows - 1)]; xassert(grid->cur_row != NULL); grid->cursor.lcf = false; grid->saved_cursor.lcf = false; /* Free sixels we failed to "map" to the new grid */ tll_foreach(untranslated_sixels, it) sixel_destroy(&it->item); tll_free(untranslated_sixels); #if defined(_DEBUG) for (int r = 0; r < new_screen_rows; r++) grid_row_in_view(grid, r); #endif } static void reflow_range_start(struct row_range *range, enum row_range_type type, struct row *new_row, int new_col_idx) { ensure_row_has_extra_data(new_row); struct row_ranges *new_ranges = NULL; switch (type) { case ROW_RANGE_URI: new_ranges = &new_row->extra->uri_ranges; break; case ROW_RANGE_UNDERLINE: new_ranges = &new_row->extra->underline_ranges; break; } if (new_ranges == NULL) BUG("unhandled range type"); range_append_by_ref(new_ranges, new_col_idx, -1, type, &range->data); switch (type) { case ROW_RANGE_URI: range->uri.uri = NULL; break; /* Owned by new_ranges */ case ROW_RANGE_UNDERLINE: break; } } static void reflow_range_end(struct row_range *range, enum row_range_type type, struct row *new_row, int new_col_idx) { struct row_data *extra = new_row->extra; struct row_ranges *ranges = NULL; switch (type) { case ROW_RANGE_URI: ranges = &extra->uri_ranges; break; case ROW_RANGE_UNDERLINE: ranges = &extra->underline_ranges; break; } if (ranges == NULL) BUG("unhandled range type"); xassert(ranges->count > 0); struct row_range *new_range = &ranges->v[ranges->count - 1]; xassert(new_range->end < 0); switch (type) { case ROW_RANGE_URI: xassert(new_range->uri.id == range->uri.id); break; case ROW_RANGE_UNDERLINE: xassert(new_range->underline.style == range->underline.style); xassert(new_range->underline.color_src == range->underline.color_src); xassert(new_range->underline.color == range->underline.color); break; } new_range->end = new_col_idx; } static struct row * _line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, int *row_idx, int *col_idx, int row_count, int col_count) { *col_idx = 0; *row_idx = (*row_idx + 1) & (row_count - 1); struct row *new_row = new_grid[*row_idx]; if (new_row == NULL) { /* Scrollback not yet full, allocate a completely new row */ new_row = grid_row_alloc(col_count, false); new_grid[*row_idx] = new_row; } else { /* Scrollback is full, need to reuse a row */ grid_row_reset_extra(new_row); new_row->shell_integration.prompt_marker = false; new_row->shell_integration.cmd_start = -1; new_row->shell_integration.cmd_end = -1; tll_foreach(old_grid->sixel_images, it) { if (it->item.pos.row == *row_idx) { sixel_destroy(&it->item); tll_remove(old_grid->sixel_images, it); } } /* * TODO: detect if the reused row is covered by the * selection. Of so, cancel the selection. The problem: we * don't know if we've translated the selection coordinates * yet. */ } struct row_data *extra = row->extra; if (extra == NULL) return new_row; /* * URI ranges are per row. Thus, we need to 'close' the still-open * ranges on the previous row, and re-open them on the * next/current row. */ if (extra->uri_ranges.count > 0) { struct row_range *range = &extra->uri_ranges.v[extra->uri_ranges.count - 1]; if (range->end < 0) { /* Terminate URI range on the previous row */ range->end = col_count - 1; /* Open a new range on the new/current row */ ensure_row_has_extra_data(new_row); range_append(&new_row->extra->uri_ranges, 0, -1, ROW_RANGE_URI, &range->data); } } if (extra->underline_ranges.count > 0) { struct row_range *range = &extra->underline_ranges.v[extra->underline_ranges.count - 1]; if (range->end < 0) { /* Terminate URI range on the previous row */ range->end = col_count - 1; /* Open a new range on the new/current row */ ensure_row_has_extra_data(new_row); range_append(&new_row->extra->underline_ranges, 0, -1, ROW_RANGE_UNDERLINE, &range->data); } } return new_row; } static struct { int scrollback_start; int rows; } tp_cmp_ctx; static int tp_cmp(const void *_a, const void *_b) { const struct coord *a = *(const struct coord **)_a; const struct coord *b = *(const struct coord **)_b; int scrollback_start = tp_cmp_ctx.scrollback_start; int num_rows = tp_cmp_ctx.rows; int a_row = (a->row - scrollback_start + num_rows) & (num_rows - 1); int b_row = (b->row - scrollback_start + num_rows) & (num_rows - 1); xassert(a_row >= 0); xassert(a_row < num_rows || num_rows == 0); xassert(b_row >= 0); xassert(b_row < num_rows || num_rows == 0); if (a_row < b_row) return -1; if (a_row > b_row) return 1; xassert(a_row == b_row); if (a->col < b->col) return -1; if (a->col > b->col) return 1; xassert(a->col == b->col); return 0; } void grid_resize_and_reflow( struct grid *grid, const struct terminal *term, int new_rows, int new_cols, int old_screen_rows, int new_screen_rows, size_t tracking_points_count, struct coord *const _tracking_points[static tracking_points_count]) { #if defined(TIME_REFLOW) && TIME_REFLOW struct timespec start; clock_gettime(CLOCK_MONOTONIC, &start); #endif struct row *const *old_grid = grid->rows; const int old_rows = grid->num_rows; const int old_cols = grid->num_cols; /* Is viewpoint tracking current grid offset? */ const bool view_follows = grid->view == grid->offset; int new_col_idx = 0; int new_row_idx = 0; struct row **new_grid = xcalloc(new_rows, sizeof(new_grid[0])); struct row *new_row = new_grid[new_row_idx]; xassert(new_row == NULL); new_row = grid_row_alloc(new_cols, false); new_grid[new_row_idx] = new_row; /* Start at the beginning of the old grid's scrollback. That is, * at the output that is *oldest* */ int offset = grid->offset + old_screen_rows; tll(struct sixel) untranslated_sixels = tll_init(); tll_foreach(grid->sixel_images, it) tll_push_back(untranslated_sixels, it->item); tll_free(grid->sixel_images); /* Turn cursor coordinates into grid absolute coordinates */ struct coord cursor = grid->cursor.point; cursor.row += grid->offset; cursor.row &= old_rows - 1; struct coord saved_cursor = grid->saved_cursor.point; saved_cursor.row += grid->offset; saved_cursor.row &= old_rows - 1; size_t tp_count = tracking_points_count + 1 + /* cursor */ 1 + /* saved cursor */ !view_follows + /* viewport */ 1; /* terminator */ struct coord *tracking_points[tp_count]; memcpy(tracking_points, _tracking_points, tracking_points_count * sizeof(_tracking_points[0])); tracking_points[tracking_points_count] = &cursor; tracking_points[tracking_points_count + 1] = &saved_cursor; struct coord viewport = {0, grid->view}; if (!view_follows) tracking_points[tracking_points_count + 2] = &viewport; /* Not thread safe! */ tp_cmp_ctx.scrollback_start = offset; tp_cmp_ctx.rows = old_rows; qsort( tracking_points, tp_count - 1, sizeof(tracking_points[0]), &tp_cmp); /* NULL terminate */ struct coord terminator = {-1, -1}; tracking_points[tp_count - 1] = &terminator; struct coord **next_tp = &tracking_points[0]; LOG_DBG("scrollback-start=%d", offset); for (size_t i = 0; i < tp_count - 1; i++) { LOG_DBG("TP #%zu: row=%d, col=%d", i, tracking_points[i]->row, tracking_points[i]->col); } int coalesced_linebreaks = 0; /* * Walk the old grid */ for (int r = 0; r < old_rows; r++) { const size_t old_row_idx = (offset + r) & (old_rows - 1); /* Unallocated (empty) rows we can simply skip */ const struct row *old_row = old_grid[old_row_idx]; if (old_row == NULL) continue; /* Map sixels on current "old" row to current "new row" */ tll_foreach(untranslated_sixels, it) { if (it->item.pos.row != old_row_idx) continue; struct sixel sixel = it->item; sixel.pos.row = new_row_idx; tll_push_back(grid->sixel_images, sixel); tll_remove(untranslated_sixels, it); } #define line_wrap() \ new_row = _line_wrap( \ grid, new_grid, new_row, &new_row_idx, &new_col_idx, \ new_rows, new_cols) /* Find last non-empty cell */ int col_count = 0; for (int c = old_cols - 1; c >= 0; c--) { const struct cell *cell = &old_row->cells[c]; if (!(cell->wc == 0 || cell->wc == CELL_SPACER)) { col_count = c + 1; break; } } if (!old_row->linebreak && col_count > 0) { /* Don't truncate logical lines */ while (col_count < old_cols && old_row->cells[col_count].wc == 0) col_count++; } xassert(col_count >= 0 && col_count <= old_cols); /* Do we have a (at least one) tracking point on this row */ struct coord *tp; if (unlikely((*next_tp)->row == old_row_idx)) { tp = *next_tp; /* Find the *last* tracking point on this row */ struct coord *last_on_row = tp; for (struct coord **iter = next_tp; (*iter)->row == old_row_idx; iter++) last_on_row = *iter; /* And make sure its end point is included in the col range */ xassert(last_on_row->row == old_row_idx); col_count = max(col_count, last_on_row->col + 1); } else tp = NULL; /* Does this row have any URIs? */ struct row_range *uri_range, *uri_range_terminator; struct row_range *underline_range, *underline_range_terminator; const struct row_data *extra = old_row->extra; if (extra != NULL && extra->uri_ranges.count > 0) { uri_range = &extra->uri_ranges.v[0]; uri_range_terminator = &extra->uri_ranges.v[extra->uri_ranges.count]; /* Make sure the *last* URI range's end point is included * in the copy */ const struct row_range *last_on_row = &extra->uri_ranges.v[extra->uri_ranges.count - 1]; col_count = max(col_count, last_on_row->end + 1); } else uri_range = uri_range_terminator = NULL; if (extra != NULL && extra->underline_ranges.count > 0) { underline_range = &extra->underline_ranges.v[0]; underline_range_terminator = &extra->underline_ranges.v[extra->underline_ranges.count]; const struct row_range *last_on_row = &extra->underline_ranges.v[extra->underline_ranges.count - 1]; col_count = max(col_count, last_on_row->end + 1); } else underline_range = underline_range_terminator = NULL; if (unlikely(col_count > 0 && coalesced_linebreaks > 0)) { for (size_t apa = 0; apa < coalesced_linebreaks; apa++) { /* 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(); } coalesced_linebreaks = 0; } for (int c = 0; c < col_count;) { const struct cell *old = &old_row->cells[c]; /* Row full, emit newline and get a new, fresh, row */ xassert(new_col_idx <= new_cols); if (unlikely(new_col_idx >= new_cols)) line_wrap(); char32_t wc = old->wc; int width = 1; if (unlikely(wc >= CELL_COMB_CHARS_LO && wc <= CELL_COMB_CHARS_HI)) { const struct composed *composed = composed_lookup(term->composed, wc - CELL_COMB_CHARS_LO); width = composed->forced_width > 0 ? composed->forced_width : composed->width; } else if (unlikely(c + 1 < col_count && (old + 1)->wc >= CELL_SPACER + 1)) { /* Wide character, get its width from the next cell's SPACER value */ width = (old + 1)->wc - CELL_SPACER + 1; } /* * Check if character fits, if not, emit spacers, and push the character to the next row */ if (unlikely(new_col_idx + width > new_cols && width <= new_cols)) { for (; new_col_idx < new_cols; new_col_idx++) { new_row->cells[new_col_idx].wc = CELL_SPACER; new_row->cells[new_col_idx].attrs = (struct attributes){0}; } line_wrap(); } new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker; for (int i = 0; i < width; i++) { if (unlikely(uri_range != NULL && uri_range != uri_range_terminator)) { if (unlikely(uri_range->start == c)) { reflow_range_start( uri_range, ROW_RANGE_URI, new_row, new_col_idx); } if (unlikely(uri_range->end == c)) { reflow_range_end( uri_range, ROW_RANGE_URI, new_row, new_col_idx); grid_row_uri_range_destroy(uri_range); uri_range++; } } if (unlikely(underline_range != NULL && underline_range != underline_range_terminator)) { if (unlikely(underline_range->start == c)) { reflow_range_start( underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx); } if (unlikely(underline_range->end == c)) { reflow_range_end( underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx); grid_row_underline_range_destroy(underline_range); underline_range++; } } if (unlikely(tp != NULL)) { if (unlikely(tp->col == c)) { do { xassert(tp->row == old_row_idx); tp->row = new_row_idx; tp->col = new_col_idx; next_tp++; tp = *next_tp; } while (tp->row == old_row_idx && tp->col == c); if (tp->row != old_row_idx) tp = NULL; LOG_DBG("next TP (tp=%p): %dx%d", (void*)tp, (*next_tp)->row, (*next_tp)->col); } } if (unlikely(old_row->shell_integration.cmd_start == c)) new_row->shell_integration.cmd_start = new_col_idx; if (unlikely(old_row->shell_integration.cmd_end == c)) new_row->shell_integration.cmd_end = new_col_idx; if (unlikely(width > new_cols)) { /* Wide character no longer fits on a row, replace it with a single space */ new_row->cells[new_col_idx++].wc = 0; c++; /* Walk past the SPACER cells */ for (int i = 1; i < width; i++, c++, old++) ; /* Continue with next character in the *old* grid */ break; } new_row->cells[new_col_idx++] = *old; /* * TODO: simulate LCF instead? * * Rows have linebreak=true by default. This is needed * for a number of reasons. However, we want non-empty * rows to have linebreak=false, *until* we reach the * end of an old row with linebreak=true, at which * point we set linebreak=true on the new row. */ new_row->linebreak = false; old++; c++; } } if (old_row->linebreak) { if (col_count > 0) { /* Erase the remaining cells */ memset(&new_row->cells[new_col_idx], 0, (new_cols - new_col_idx) * sizeof(new_row->cells[0])); new_row->linebreak = true; if (r + 1 < old_rows) { /* Not the last (old) row */ line_wrap(); } else if (new_row->extra != NULL) { if (new_row->extra->uri_ranges.count > 0) { /* * line_wrap() "closes" still-open URIs. Since * this is the *last* row, and since we're * line-breaking due to a hard line-break (rather * than running out of cells in the "new_row"), * there shouldn't be an open URI (it would have * been closed when we reached the end of the URI * while reflowing the last "old" row). */ int last_idx = new_row->extra->uri_ranges.count - 1; xassert(new_row->extra->uri_ranges.v[last_idx].end >= 0); } if (new_row->extra->underline_ranges.count > 0) { int last_idx = new_row->extra->underline_ranges.count - 1; xassert(new_row->extra->underline_ranges.v[last_idx].end >= 0); } } } else { /* * rows have linebreak=true by default. But we don't * want trailing empty lines to result in actual lines * in the new grid (think: empty window with prompt at * the top) */ coalesced_linebreaks++; } } grid_row_free(old_grid[old_row_idx]); grid->rows[old_row_idx] = NULL; #undef line_wrap } /* Erase the remaining cells */ memset(&new_row->cells[new_col_idx], 0, (new_cols - new_col_idx) * sizeof(new_row->cells[0])); for (struct coord **tp = next_tp; *tp != &terminator; tp++) { LOG_DBG("TP: row=%d, col=%d (old cols: %d, new cols: %d)", (*tp)->row, (*tp)->col, old_cols, new_cols); } xassert(old_rows == 0 || *next_tp == &terminator); #if defined(_DEBUG) /* Verify all URI ranges have been "closed" */ for (int r = 0; r < new_rows; r++) { const struct row *row = new_grid[r]; if (row == NULL) continue; if (row->extra == NULL) continue; for (size_t i = 0; i < row->extra->uri_ranges.count; i++) xassert(row->extra->uri_ranges.v[i].end >= 0); for (size_t i = 0; i < row->extra->underline_ranges.count; i++) xassert(row->extra->underline_ranges.v[i].end >= 0); verify_no_overlapping_ranges(row->extra); verify_ranges_are_sorted(row->extra); } /* Verify all old rows have been free:d */ for (int i = 0; i < old_rows; i++) xassert(grid->rows[i] == NULL); #endif /* Set offset such that the last reflowed row is at the bottom */ grid->offset = new_row_idx - new_screen_rows + 1; while (grid->offset < 0) grid->offset += new_rows; while (new_grid[grid->offset] == NULL) grid->offset = (grid->offset + 1) & (new_rows - 1); /* Ensure all visible rows have been allocated */ for (int r = 0; r < new_screen_rows; r++) { int idx = (grid->offset + r) & (new_rows - 1); if (new_grid[idx] == NULL) new_grid[idx] = grid_row_alloc(new_cols, true); } /* Free old grid (rows already free:d) */ free(grid->rows); grid->rows = new_grid; grid->num_rows = new_rows; grid->num_cols = new_cols; /* * Set new viewport, making sure it's not too far down. * * This is done by using scrollback-start relative cooardinates, * and bounding the new viewport to (grid_rows - screen_rows). */ int sb_view = grid_row_abs_to_sb( grid, new_screen_rows, view_follows ? grid->offset : viewport.row); grid->view = grid_row_sb_to_abs( grid, new_screen_rows, min(sb_view, new_rows - new_screen_rows)); /* Convert absolute coordinates to screen relative */ cursor.row -= grid->offset; while (cursor.row < 0) cursor.row += grid->num_rows; cursor.row = min(cursor.row, new_screen_rows - 1); cursor.col = min(cursor.col, new_cols - 1); saved_cursor.row -= grid->offset; while (saved_cursor.row < 0) saved_cursor.row += grid->num_rows; saved_cursor.row = min(saved_cursor.row, new_screen_rows - 1); saved_cursor.col = min(saved_cursor.col, new_cols - 1); if (grid->cursor.lcf) { if (cursor.col + 1 < new_cols) { cursor.col++; grid->cursor.lcf = false; } } if (grid->saved_cursor.lcf) { if (saved_cursor.col + 1 < new_cols) { saved_cursor.col++; grid->saved_cursor.lcf = false; } } grid->cur_row = new_grid[(grid->offset + cursor.row) & (new_rows - 1)]; xassert(grid->cur_row != NULL); grid->cursor.point = cursor; grid->saved_cursor.point = saved_cursor; /* Free sixels we failed to "map" to the new grid */ tll_foreach(untranslated_sixels, it) sixel_destroy(&it->item); tll_free(untranslated_sixels); #if defined(TIME_REFLOW) && TIME_REFLOW struct timespec stop; clock_gettime(CLOCK_MONOTONIC, &stop); struct timespec diff; timespec_sub(&stop, &start, &diff); LOG_INFO("reflowed %d -> %d rows in %lds %ldns", old_rows, new_rows, (long)diff.tv_sec, diff.tv_nsec); #endif } static bool ranges_match(const struct row_range *r1, const struct row_range *r2, enum row_range_type type) { switch (type) { case ROW_RANGE_URI: /* TODO: also match URI? */ return r1->uri.id == r2->uri.id; case ROW_RANGE_UNDERLINE: return r1->underline.style == r2->underline.style && r1->underline.color_src == r2->underline.color_src && r1->underline.color == r2->underline.color; } BUG("invalid range type"); return false; } static bool range_match_data(const struct row_range *r, const union row_range_data *data, enum row_range_type type) { switch (type) { case ROW_RANGE_URI: return r->uri.id == data->uri.id; case ROW_RANGE_UNDERLINE: return r->underline.style == data->underline.style && r->underline.color_src == data->underline.color_src && r->underline.color == data->underline.color; } BUG("invalid range type"); return false; } static void grid_row_range_put(struct row_ranges *ranges, int col, const union row_range_data *data, enum row_range_type type) { size_t insert_idx = 0; bool replace = false; bool run_merge_pass = false; for (int i = ranges->count - 1; i >= 0; i--) { struct row_range *r = &ranges->v[i]; const bool matching = range_match_data(r, data, type); if (matching && r->end + 1 == col) { /* Extend existing range tail */ r->end++; return; } else if (r->end < col) { insert_idx = i + 1; break; } else if (r->start > col) continue; else { xassert(r->start <= col); xassert(r->end >= col); if (matching) return; if (r->start == r->end) { replace = true; run_merge_pass = true; insert_idx = i; } else if (r->start == col) { run_merge_pass = true; r->start++; insert_idx = i; } else if (r->end == col) { run_merge_pass = true; r->end--; insert_idx = i + 1; } else { xassert(r->start < col); xassert(r->end > col); union row_range_data insert_data; switch (type) { case ROW_RANGE_URI: insert_data.uri = r->uri; break; case ROW_RANGE_UNDERLINE: insert_data.underline = r->underline; break; } range_insert(ranges, i + 1, col + 1, r->end, type, &insert_data); /* The insertion may xrealloc() the vector, making our * 'old' pointer invalid */ r = &ranges->v[i]; r->end = col - 1; xassert(r->start <= r->end); insert_idx = i + 1; } break; } } xassert(insert_idx <= ranges->count); if (replace) { grid_row_range_destroy(&ranges->v[insert_idx], type); ranges->v[insert_idx] = (struct row_range){ .start = col, .end = col, }; switch (type) { case ROW_RANGE_URI: ranges->v[insert_idx].uri.id = data->uri.id; ranges->v[insert_idx].uri.uri = xstrdup(data->uri.uri); break; case ROW_RANGE_UNDERLINE: ranges->v[insert_idx].underline = data->underline; break; } } else range_insert(ranges, insert_idx, col, col, type, data); if (run_merge_pass) { for (size_t i = 1; i < ranges->count; i++) { struct row_range *r1 = &ranges->v[i - 1]; struct row_range *r2 = &ranges->v[i]; if (ranges_match(r1, r2, type) && r1->end + 1 == r2->start) { r1->end = r2->end; range_delete(ranges, type, i); i--; } } } } void grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) { ensure_row_has_extra_data(row); grid_row_range_put( &row->extra->uri_ranges, col, &(union row_range_data){.uri = {.id = id, .uri = (char *)uri}}, ROW_RANGE_URI); verify_no_overlapping_ranges(row->extra); verify_ranges_are_sorted(row->extra); } void grid_row_underline_range_put(struct row *row, int col, struct underline_range_data data) { ensure_row_has_extra_data(row); grid_row_range_put( &row->extra->underline_ranges, col, &(union row_range_data){.underline = data}, ROW_RANGE_UNDERLINE); verify_no_overlapping_ranges(row->extra); verify_ranges_are_sorted(row->extra); } UNITTEST { struct row_data row_data = {.uri_ranges = {0}}; struct row row = {.extra = &row_data}; #define verify_range(idx, _start, _end, _id) \ do { \ xassert(idx < row_data.uri_ranges.count); \ xassert(row_data.uri_ranges.v[idx].start == _start); \ xassert(row_data.uri_ranges.v[idx].end == _end); \ xassert(row_data.uri_ranges.v[idx].uri.id == _id); \ } while (0) grid_row_uri_range_put(&row, 0, "http://foo.bar", 123); grid_row_uri_range_put(&row, 1, "http://foo.bar", 123); grid_row_uri_range_put(&row, 2, "http://foo.bar", 123); grid_row_uri_range_put(&row, 3, "http://foo.bar", 123); xassert(row_data.uri_ranges.count == 1); verify_range(0, 0, 3, 123); /* No-op */ grid_row_uri_range_put(&row, 0, "http://foo.bar", 123); xassert(row_data.uri_ranges.count == 1); verify_range(0, 0, 3, 123); /* Replace head */ grid_row_uri_range_put(&row, 0, "http://head", 456); xassert(row_data.uri_ranges.count == 2); verify_range(0, 0, 0, 456); verify_range(1, 1, 3, 123); /* Replace tail */ grid_row_uri_range_put(&row, 3, "http://tail", 789); xassert(row_data.uri_ranges.count == 3); verify_range(1, 1, 2, 123); verify_range(2, 3, 3, 789); /* Replace tail + extend head */ grid_row_uri_range_put(&row, 2, "http://tail", 789); xassert(row_data.uri_ranges.count == 3); verify_range(1, 1, 1, 123); verify_range(2, 2, 3, 789); /* Replace + extend tail */ grid_row_uri_range_put(&row, 1, "http://head", 456); xassert(row_data.uri_ranges.count == 2); verify_range(0, 0, 1, 456); verify_range(1, 2, 3, 789); /* Replace + extend, then splice */ grid_row_uri_range_put(&row, 1, "http://tail", 789); grid_row_uri_range_put(&row, 2, "http://splice", 000); xassert(row_data.uri_ranges.count == 4); verify_range(0, 0, 0, 456); verify_range(1, 1, 1, 789); verify_range(2, 2, 2, 000); verify_range(3, 3, 3, 789); for (size_t i = 0; i < row_data.uri_ranges.count; i++) grid_row_uri_range_destroy(&row_data.uri_ranges.v[i]); free(row_data.uri_ranges.v); #undef verify_range } static void grid_row_range_erase(struct row_ranges *ranges, enum row_range_type type, int start, int end) { xassert(start <= end); /* Split up, or remove, URI ranges affected by the erase */ for (int i = ranges->count - 1; i >= 0; i--) { struct row_range *old = &ranges->v[i]; if (old->end < start) return; if (old->start > end) continue; if (start <= old->start && end >= old->end) { /* Erase range covers URI completely - remove it */ range_delete(ranges, type, i); } else if (start > old->start && end < old->end) { /* * Erase range erases a part in the middle of the URI * * Must copy, since range_insert() may xrealloc() (thus * causing 'old' to be invalid) before it dereferences * old->data */ union row_range_data data = old->data; range_insert(ranges, i + 1, end + 1, old->end, type, &data); /* The insertion may xrealloc() the vector, making our * 'old' pointer invalid */ old = &ranges->v[i]; old->end = start - 1; return; /* There can be no more URIs affected by the erase range */ } else if (start <= old->start && end >= old->start) { /* Erase range erases the head of the URI */ xassert(start <= old->start); old->start = end + 1; } else if (start <= old->end && end >= old->end) { /* Erase range erases the tail of the URI */ xassert(end >= old->end); old->end = start - 1; return; /* There can be no more overlapping URIs */ } } } void grid_row_uri_range_erase(struct row *row, int start, int end) { xassert(row->extra != NULL); grid_row_range_erase(&row->extra->uri_ranges, ROW_RANGE_URI, start, end); } void grid_row_underline_range_erase(struct row *row, int start, int end) { xassert(row->extra != NULL); grid_row_range_erase(&row->extra->underline_ranges, ROW_RANGE_UNDERLINE, start, end); } UNITTEST { struct row_data row_data = {.uri_ranges = {0}}; struct row row = {.extra = &row_data}; const union row_range_data data = { .uri = { .id = 0, .uri = (char *)"dummy", }, }; /* Try erasing a row without any URIs */ grid_row_uri_range_erase(&row, 0, 200); xassert(row_data.uri_ranges.count == 0); range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); range_append(&row_data.uri_ranges, 11, 20, ROW_RANGE_URI, &data); xassert(row_data.uri_ranges.count == 2); xassert(row_data.uri_ranges.v[1].start == 11); xassert(row_data.uri_ranges.v[1].end == 20); verify_no_overlapping_ranges(&row_data); verify_ranges_are_sorted(&row_data); /* Erase both URis */ grid_row_uri_range_erase(&row, 1, 20); xassert(row_data.uri_ranges.count == 0); verify_no_overlapping_ranges(&row_data); verify_ranges_are_sorted(&row_data); /* Two URIs, then erase second half of the first, first half of the second */ range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); range_append(&row_data.uri_ranges, 11, 20, ROW_RANGE_URI, &data); grid_row_uri_range_erase(&row, 5, 15); xassert(row_data.uri_ranges.count == 2); xassert(row_data.uri_ranges.v[0].start == 1); xassert(row_data.uri_ranges.v[0].end == 4); xassert(row_data.uri_ranges.v[1].start == 16); xassert(row_data.uri_ranges.v[1].end == 20); verify_no_overlapping_ranges(&row_data); verify_ranges_are_sorted(&row_data); grid_row_range_destroy(&row_data.uri_ranges.v[0], ROW_RANGE_URI); grid_row_range_destroy(&row_data.uri_ranges.v[1], ROW_RANGE_URI); row_data.uri_ranges.count = 0; /* One URI, erase middle part of it */ range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); grid_row_uri_range_erase(&row, 5, 6); xassert(row_data.uri_ranges.count == 2); xassert(row_data.uri_ranges.v[0].start == 1); xassert(row_data.uri_ranges.v[0].end == 4); xassert(row_data.uri_ranges.v[1].start == 7); xassert(row_data.uri_ranges.v[1].end == 10); verify_no_overlapping_ranges(&row_data); verify_ranges_are_sorted(&row_data); grid_row_range_destroy(&row_data.uri_ranges.v[0], ROW_RANGE_URI); grid_row_range_destroy(&row_data.uri_ranges.v[1], ROW_RANGE_URI); row_data.uri_ranges.count = 0; /* * Regression test: erasing the middle part of an URI causes us to * insert a new URI (we split the partly erased URI into two). * * The insertion logic typically triggers an xrealloc(), which, in * some cases, *moves* the entire URI vector to a new base * address. grid_row_uri_range_erase() did not account for this, * and tried to update the 'end' member in the URI range we just * split. This causes foot to crash when the xrealloc() has moved * the URI range vector. * * (note: we're only verifying we don't crash here, hence the lack * of assertions). */ free(row_data.uri_ranges.v); row_data.uri_ranges.v = NULL; row_data.uri_ranges.size = 0; range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); xassert(row_data.uri_ranges.size == 1); grid_row_uri_range_erase(&row, 5, 7); xassert(row_data.uri_ranges.count == 2); grid_row_ranges_destroy(&row_data.uri_ranges, ROW_RANGE_URI); free(row_data.uri_ranges.v); } �����������������������������foot-1.21.0/grid.h����������������������������������������������������������������������������������0000664�0000000�0000000�00000007403�14766001452�0013651�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#pragma once #include <stddef.h> #include "debug.h" #include "terminal.h" struct grid *grid_snapshot(const struct grid *grid); void grid_free(struct grid *grid); void grid_swap_row(struct grid *grid, int row_a, int row_b); struct row *grid_row_alloc(int cols, bool initialize); void grid_row_free(struct row *row); void grid_resize_without_reflow( struct grid *grid, int new_rows, int new_cols, int old_screen_rows, int new_screen_rows); void grid_resize_and_reflow( struct grid *grid, const struct terminal *term, int new_rows, int new_cols, int old_screen_rows, int new_screen_rows, size_t tracking_points_count, struct coord *const _tracking_points[static tracking_points_count]); /* Convert row numbers between scrollback-relative and absolute coordinates */ int grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row); int grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row); int grid_sb_start_ignore_uninitialized(const struct grid *grid, int screen_rows); int grid_row_abs_to_sb_precalc_sb_start( const struct grid *grid, int sb_start, int abs_row); int grid_row_sb_to_abs_precalc_sb_start( const struct grid *grid, int sb_start, int sb_rel_row); static inline int grid_row_absolute(const struct grid *grid, int row_no) { return (grid->offset + row_no) & (grid->num_rows - 1); } static inline int grid_row_absolute_in_view(const struct grid *grid, int row_no) { return (grid->view + row_no) & (grid->num_rows - 1); } static inline struct row * _grid_row_maybe_alloc(struct grid *grid, int row_no, bool alloc_if_null) { xassert(grid->offset >= 0); int real_row = grid_row_absolute(grid, row_no); struct row *row = grid->rows[real_row]; if (row == NULL && alloc_if_null) { row = grid_row_alloc(grid->num_cols, false); grid->rows[real_row] = row; } xassert(row != NULL); return row; } static inline struct row * grid_row(struct grid *grid, int row_no) { return _grid_row_maybe_alloc(grid, row_no, false); } static inline struct row * grid_row_and_alloc(struct grid *grid, int row_no) { return _grid_row_maybe_alloc(grid, row_no, true); } static inline struct row * grid_row_in_view(struct grid *grid, int row_no) { xassert(grid->view >= 0); int real_row = grid_row_absolute_in_view(grid, row_no); struct row *row = grid->rows[real_row]; xassert(row != NULL); return row; } void grid_row_uri_range_put( struct row *row, int col, const char *uri, uint64_t id); void grid_row_uri_range_erase(struct row *row, int start, int end); void grid_row_underline_range_put( struct row *row, int col, struct underline_range_data data); void grid_row_underline_range_erase(struct row *row, int start, int end); static inline void grid_row_uri_range_destroy(struct row_range *range) { free(range->uri.uri); } static inline void grid_row_underline_range_destroy(struct row_range *range) { } static inline void grid_row_range_destroy(struct row_range *range, enum row_range_type type) { switch (type) { case ROW_RANGE_URI: grid_row_uri_range_destroy(range); break; case ROW_RANGE_UNDERLINE: grid_row_underline_range_destroy(range); break; } } static inline void grid_row_ranges_destroy(struct row_ranges *ranges, enum row_range_type type) { for (int i = 0; i < ranges->count; i++) { grid_row_range_destroy(&ranges->v[i], type); } } static inline void grid_row_reset_extra(struct row *row) { struct row_data *extra = row->extra; if (likely(extra == NULL)) return; grid_row_ranges_destroy(&extra->uri_ranges, ROW_RANGE_URI); grid_row_ranges_destroy(&extra->underline_ranges, ROW_RANGE_UNDERLINE); free(extra->uri_ranges.v); free(extra->underline_ranges.v); free(extra); row->extra = NULL; } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/hsl.c�����������������������������������������������������������������������������������0000664�0000000�0000000�00000003473�14766001452�0013510�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#include "hsl.h" #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) { double L = lum / 100.0; double S = sat / 100.0; double C = (1. - fabs(2. * L - 1.)) * S; double X = C * (1. - fabs(fmod((double)hue / 60., 2.) - 1.)); double m = L - C / 2.; double r, g, b; if (hue >= 0 && hue <= 60) { r = C; g = X; b = 0.; } else if (hue >= 60 && hue <= 120) { r = X; g = C; b = 0.; } else if (hue >= 120 && hue <= 180) { r = 0.; g = C; b = X; } else if (hue >= 180 && hue <= 240) { r = 0.; g = X; b = C; } else if (hue >= 240 && hue <= 300) { r = X; g = 0.; b = C; } else if (hue >= 300 && hue <= 360) { r = C; g = 0.; b = X; } else { r = 0.; g = 0.; b = 0.; } r += m; g += m; b += m; return ( (uint8_t)round(r * 255.) << 16 | (uint8_t)round(g * 255.) << 8 | (uint8_t)round(b * 255.) << 0); } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/hsl.h�����������������������������������������������������������������������������������0000664�0000000�0000000�00000000220�14766001452�0013500�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#pragma once #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); ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/icons/����������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14766001452�0013662�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/icons/hicolor/��������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14766001452�0015321�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/icons/hicolor/48x48/��������������������������������������������������������������������0000775�0000000�0000000�00000000000�14766001452�0016120�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/icons/hicolor/48x48/apps/���������������������������������������������������������������0000775�0000000�0000000�00000000000�14766001452�0017063�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/icons/hicolor/48x48/apps/foot.png�������������������������������������������������������0000664�0000000�0000000�00000001721�14766001452�0020541�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR���0���0���W���bKGD�����IDATh_lE?{*%Ш`IPC@hPQcJLh`bM IlUrj=)mWJM9ڭݻcٽ3q?O|#e+C #L_y{z5`Y@v\v#x|ϨZ#FUhPhU%MM ( ˑ?ߤ]'J::cL@UU(8X$io7wPdO1l7ٿvsrNඊhhSQQql 0LNg5ܹ]/6"IY' ykD7 >3$'3r# Zw[D�9#|qܼ`PlO΂ xvIxnuy*�?x2cm%rS)7u-6vQ, Ӵ<ExWvizZs0Ms1Q�L$'C)ӘBnq�lajфg2U{V~Zt;<乔WmyںG8=vmKG}Eh=̣M3NF X4zd4=>ϿԻ6B:b#�y-L̦y9<wa N~bz09e9W={x.',4>1Ǽq @  @ EɧGNr8H�ѪJܱ:ZKc7Z<j9P  yĴ:ZEcc=@J#>^vgGQRe5m^z:[fjZFr#L{_e}'.����IENDB`�����������������������������������������������foot-1.21.0/icons/hicolor/scalable/�����������������������������������������������������������������0000775�0000000�0000000�00000000000�14766001452�0017067�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/icons/hicolor/scalable/apps/������������������������������������������������������������0000775�0000000�0000000�00000000000�14766001452�0020032�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������foot-1.21.0/icons/hicolor/scalable/apps/foot.svg����������������������������������������������������0000664�0000000�0000000�00000006135�14766001452�0021527�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="128" height="128" inkscape:version="1.0 (4035a4fb49, 2020-05-01)"> <title>foot logo image/svg+xml Lennard Hofmann https://freesvg.org/human-footprints foot logo terminal emulator footprint 2020-06-23 Black square representing a terminal showing a human footprint as a prompt symbol and an underscore as the cursor foot-1.21.0/icons/meson.build000066400000000000000000000001241476600145200160210ustar00rootroot00000000000000install_subdir('hicolor', install_dir : join_paths(get_option('datadir'), 'icons')) foot-1.21.0/ime.c000066400000000000000000000354741476600145200135020ustar00rootroot00000000000000#include "ime.h" #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED #include #include "text-input-unstable-v3.h" #define LOG_MODULE "ime" #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" #include "render.h" #include "search.h" #include "terminal.h" #include "util.h" #include "wayland.h" #include "xmalloc.h" static void ime_reset_pending_preedit(struct seat *seat) { free(seat->ime.preedit.pending.text); seat->ime.preedit.pending.text = NULL; } static void ime_reset_pending_commit(struct seat *seat) { free(seat->ime.commit.pending.text); seat->ime.commit.pending.text = NULL; } void ime_reset_pending(struct seat *seat) { ime_reset_pending_preedit(seat); ime_reset_pending_commit(seat); } void ime_reset_preedit(struct seat *seat) { if (seat->ime.preedit.cells == NULL) return; free(seat->ime.preedit.text); free(seat->ime.preedit.cells); seat->ime.preedit.text = NULL; seat->ime.preedit.cells = NULL; seat->ime.preedit.count = 0; } static void enter(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, struct wl_surface *surface) { struct seat *seat = data; struct wl_window *win = wl_surface_get_user_data(surface); struct terminal *term = win->term; LOG_DBG("enter: seat=%s, term=%p", seat->name, (const void *)term); if (seat->kbd_focus != term) { LOG_WARN("compositor sent ime::enter() event before the " "corresponding keyboard_enter() event"); } /* The main grid is the *only* input-receiving surface we have */ seat->ime_focus = term; const struct coord *cursor = &term->grid->cursor.point; term_ime_set_cursor_rect( term, term->margins.left + cursor->col * term->cell_width, term->margins.top + cursor->row * term->cell_height, term->cell_width, term->cell_height); ime_enable(seat); } static void leave(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, struct wl_surface *surface) { struct seat *seat = data; LOG_DBG("leave: seat=%s", seat->name); ime_disable(seat); seat->ime_focus = NULL; } static void preedit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, const char *text, int32_t cursor_begin, int32_t cursor_end) { LOG_DBG("preedit-string: text=%s, begin=%d, end=%d", text, cursor_begin, cursor_end); struct seat *seat = data; ime_reset_pending_preedit(seat); if (text != NULL) { seat->ime.preedit.pending.text = xstrdup(text); seat->ime.preedit.pending.cursor_begin = cursor_begin; seat->ime.preedit.pending.cursor_end = cursor_end; } } static void commit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, const char *text) { LOG_DBG("commit: text=%s", text); struct seat *seat = data; ime_reset_pending_commit(seat); if (text != NULL) seat->ime.commit.pending.text = xstrdup(text); } static void delete_surrounding_text(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t before_length, uint32_t after_length) { LOG_DBG("delete-surrounding: before=%d, after=%d", before_length, after_length); struct seat *seat = data; seat->ime.surrounding.pending.before_length = before_length; seat->ime.surrounding.pending.after_length = after_length; } static void done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t serial) { /* * From text-input-unstable-v3.h: * * The application must proceed by evaluating the changes in the * following order: * * 1. Replace existing preedit string with the cursor. * 2. Delete requested surrounding text. * 3. Insert commit string with the cursor at its end. * 4. Calculate surrounding text to send. * 5. Insert new preedit text in cursor position. * 6. Place cursor inside preedit text. */ LOG_DBG("done: serial=%u", serial); struct seat *seat = data; struct terminal *term = seat->ime_focus; if (seat->ime.serial != serial) { LOG_DBG("IME serial mismatch: expected=0x%08x, got 0x%08x", seat->ime.serial, serial); return; } if (term == NULL) { static bool have_warned = false; if (!have_warned) { LOG_WARN( "%s: text-input::done() received on seat that isn't " "focusing a terminal window", seat->name); have_warned = true; } } /* 1. Delete existing pre-edit text */ if (seat->ime.preedit.cells != NULL) { ime_reset_preedit(seat); if (term != NULL) { if (term->is_searching) render_refresh_search(term); else render_refresh(term); } } /* * 2. Delete requested surrounding text * * We don't support deleting surrounding text. But, we also never * call set_surrounding_text() so hopefully we should never * receive any requests to delete surrounding text. */ /* 3. Insert commit string */ if (seat->ime.commit.pending.text != NULL) { const char *text = seat->ime.commit.pending.text; size_t len = strlen(text); if (term != NULL) { if (term->is_searching) { search_add_chars(term, text, len); render_refresh_search(term); } else term_to_slave(term, text, len); } ime_reset_pending_commit(seat); } /* 4. Calculate surrounding text to send - not supported */ /* 5. Insert new pre-edit text */ char32_t *allocated_preedit_text = NULL; if (seat->ime.preedit.pending.text == NULL || seat->ime.preedit.pending.text[0] == '\0' || (allocated_preedit_text = ambstoc32(seat->ime.preedit.pending.text)) == NULL) { ime_reset_pending_preedit(seat); return; } xassert(seat->ime.preedit.pending.text != NULL); xassert(allocated_preedit_text != NULL); seat->ime.preedit.text = allocated_preedit_text; size_t wchars = c32len(seat->ime.preedit.text); /* Next, count number of cells needed */ size_t cell_count = 0; size_t widths[wchars + 1]; for (size_t i = 0; i < wchars; i++) { int width = max(c32width(seat->ime.preedit.text[i]), 1); widths[i] = width; cell_count += width; } /* Allocate cells */ seat->ime.preedit.cells = xmalloc( cell_count * sizeof(seat->ime.preedit.cells[0])); seat->ime.preedit.count = cell_count; /* Populate cells */ for (size_t i = 0, cell_idx = 0; i < wchars; i++) { struct cell *cell = &seat->ime.preedit.cells[cell_idx]; int width = widths[i]; cell->wc = seat->ime.preedit.text[i]; cell->attrs = (struct attributes){.clean = 0}; for (int j = 1; j < width; j++) { cell = &seat->ime.preedit.cells[cell_idx + j]; cell->wc = CELL_SPACER + width - j; cell->attrs = (struct attributes){.clean = 1}; } cell_idx += width; } const size_t byte_len = strlen(seat->ime.preedit.pending.text); /* Pre-edit cursor - hidden */ if (seat->ime.preedit.pending.cursor_begin == -1 || seat->ime.preedit.pending.cursor_end == -1) { /* Note: docs says *both* begin and end should be -1, * but what else can we do if only one is -1? */ LOG_DBG("pre-edit cursor is hidden"); seat->ime.preedit.cursor.hidden = true; seat->ime.preedit.cursor.start = -1; seat->ime.preedit.cursor.end = -1; } else if (seat->ime.preedit.pending.cursor_begin == byte_len && seat->ime.preedit.pending.cursor_end == byte_len) { /* Cursor is *after* the entire pre-edit string */ seat->ime.preedit.cursor.hidden = false; seat->ime.preedit.cursor.start = cell_count; seat->ime.preedit.cursor.end = cell_count; } else { /* * Translate cursor position to cell indices * * The cursor_begin and cursor_end are counted in * *bytes*. We want to map them to *cell* indices. * * To do this, we use mblen() to step though the utf-8 * pre-edit string, advancing a unicode character index as * we go, *and* advancing a *cell* index using c32width() * of the unicode character. * * When we find the matching *byte* index, we at the same * time know both the unicode *and* cell index. */ int cell_begin = -1, cell_end = -1; for (size_t byte_idx = 0, wc_idx = 0, cell_idx = 0; byte_idx < byte_len && wc_idx < wchars && cell_idx < cell_count && (cell_begin < 0 || cell_end < 0); cell_idx += widths[wc_idx], wc_idx++) { if (seat->ime.preedit.pending.cursor_begin == byte_idx) cell_begin = cell_idx; if (seat->ime.preedit.pending.cursor_end == byte_idx) cell_end = cell_idx; /* Number of bytes of *next* utf-8 character */ size_t left = byte_len - byte_idx; int wc_bytes = mblen(&seat->ime.preedit.pending.text[byte_idx], left); if (wc_bytes <= 0) break; byte_idx += wc_bytes; } if (seat->ime.preedit.pending.cursor_end >= byte_len) cell_end = cell_count; /* Bounded by number of screen columns */ cell_begin = min(max(cell_begin, 0), cell_count - 1); cell_end = min(max(cell_end, 0), cell_count); if (cell_end < cell_begin) cell_end = cell_begin; /* Expand cursor end to end of glyph */ while (cell_end > cell_begin && cell_end < cell_count && seat->ime.preedit.cells[cell_end].wc >= CELL_SPACER) { cell_end++; } LOG_DBG("pre-edit cursor: begin=%d, end=%d", cell_begin, cell_end); xassert(cell_begin >= 0); xassert(cell_begin < cell_count); xassert(cell_begin <= cell_end); xassert(cell_end >= 0); xassert(cell_end <= cell_count); seat->ime.preedit.cursor.hidden = false; seat->ime.preedit.cursor.start = cell_begin; seat->ime.preedit.cursor.end = cell_end; } /* Underline pre-edit string that is *not* covered by the cursor */ bool hidden = seat->ime.preedit.cursor.hidden; int start = seat->ime.preedit.cursor.start; int end = seat->ime.preedit.cursor.end; for (size_t i = 0, cell_idx = 0; i < wchars; cell_idx += widths[i], i++) { if (hidden || start == end || cell_idx < start || cell_idx >= end) { struct cell *cell = &seat->ime.preedit.cells[cell_idx]; cell->attrs.underline = true; } } ime_reset_pending_preedit(seat); if (term != NULL) { if (term->is_searching) render_refresh_search(term); else render_refresh(term); } } static void ime_send_cursor_rect(struct seat *seat) { if (unlikely(seat->wayl->text_input_manager == NULL)) return; if (seat->ime_focus == NULL) return; struct terminal *term = seat->ime_focus; if (!term->ime_enabled) return; if (seat->ime.cursor_rect.pending.x == seat->ime.cursor_rect.sent.x && seat->ime.cursor_rect.pending.y == seat->ime.cursor_rect.sent.y && seat->ime.cursor_rect.pending.width == seat->ime.cursor_rect.sent.width && seat->ime.cursor_rect.pending.height == seat->ime.cursor_rect.sent.height) { return; } zwp_text_input_v3_set_cursor_rectangle( seat->wl_text_input, seat->ime.cursor_rect.pending.x / term->scale, seat->ime.cursor_rect.pending.y / term->scale, seat->ime.cursor_rect.pending.width / term->scale, seat->ime.cursor_rect.pending.height / term->scale); zwp_text_input_v3_commit(seat->wl_text_input); seat->ime.serial++; seat->ime.cursor_rect.sent = seat->ime.cursor_rect.pending; } void ime_enable(struct seat *seat) { if (unlikely(seat->wayl->text_input_manager == NULL)) return; if (seat->ime_focus == NULL) return; struct terminal *term = seat->ime_focus; if (term == NULL) return; if (!term->ime_enabled) return; ime_reset_pending(seat); ime_reset_preedit(seat); zwp_text_input_v3_enable(seat->wl_text_input); zwp_text_input_v3_set_content_type( seat->wl_text_input, ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL); zwp_text_input_v3_set_cursor_rectangle( seat->wl_text_input, seat->ime.cursor_rect.pending.x / term->scale, seat->ime.cursor_rect.pending.y / term->scale, seat->ime.cursor_rect.pending.width / term->scale, seat->ime.cursor_rect.pending.height / term->scale); seat->ime.cursor_rect.sent = seat->ime.cursor_rect.pending; zwp_text_input_v3_commit(seat->wl_text_input); seat->ime.serial++; } void ime_disable(struct seat *seat) { if (unlikely(seat->wayl->text_input_manager == NULL)) return; if (seat->ime_focus == NULL) return; ime_reset_pending(seat); ime_reset_preedit(seat); zwp_text_input_v3_disable(seat->wl_text_input); zwp_text_input_v3_commit(seat->wl_text_input); seat->ime.serial++; } void ime_update_cursor_rect(struct seat *seat) { struct terminal *term = seat->ime_focus; /* Set in render_ime_preedit() */ if (seat->ime.preedit.cells != NULL) goto update; /* Set in render_search_box() */ if (term->is_searching) goto update; int x, y, width, height; int col = term->grid->cursor.point.col; int row = term->grid->cursor.point.row; row += term->grid->offset; row -= term->grid->view; row &= term->grid->num_rows - 1; x = term->margins.left + col * term->cell_width; y = term->margins.top + row * term->cell_height; if (term->cursor_style == CURSOR_BEAM) width = 1; else width = term->cell_width; height = term->cell_height; seat->ime.cursor_rect.pending.x = x; seat->ime.cursor_rect.pending.y = y; seat->ime.cursor_rect.pending.width = width; seat->ime.cursor_rect.pending.height = height; update: ime_send_cursor_rect(seat); } const struct zwp_text_input_v3_listener text_input_listener = { .enter = &enter, .leave = &leave, .preedit_string = &preedit_string, .commit_string = &commit_string, .delete_surrounding_text = &delete_surrounding_text, .done = &done, }; #else /* !FOOT_IME_ENABLED */ void ime_enable(struct seat *seat) {} void ime_disable(struct seat *seat) {} void ime_update_cursor_rect(struct seat *seat) {} void ime_reset_pending_preedit(struct seat *seat) {} void ime_reset_pending_commit(struct seat *seat) {} void ime_reset_pending(struct seat *seat) {} void ime_reset_preedit(struct seat *seat) {} #endif foot-1.21.0/ime.h000066400000000000000000000006711476600145200134760ustar00rootroot00000000000000#pragma once #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED #include "text-input-unstable-v3.h" extern const struct zwp_text_input_v3_listener text_input_listener; #endif /* FOOT_IME_ENABLED */ struct seat; struct terminal; void ime_enable(struct seat *seat); void ime_disable(struct seat *seat); void ime_update_cursor_rect(struct seat *seat); void ime_reset_pending(struct seat *seat); void ime_reset_preedit(struct seat *seat); foot-1.21.0/input.c000066400000000000000000003551311476600145200140620ustar00rootroot00000000000000#include "input.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "input" #define LOG_ENABLE_DBG 0 #include "log.h" #include "commands.h" #include "config.h" #include "grid.h" #include "keymap.h" #include "kitty-keymap.h" #include "macros.h" #include "quirks.h" #include "render.h" #include "search.h" #include "selection.h" #include "spawn.h" #include "terminal.h" #include "tokenize.h" #include "unicode-mode.h" #include "url-mode.h" #include "util.h" #include "vt.h" #include "xmalloc.h" #include "xsnprintf.h" struct pipe_context { char *text; size_t idx; size_t left; }; static bool fdm_write_pipe(struct fdm *fdm, int fd, int events, void *data) { struct pipe_context *ctx = data; if (events & EPOLLHUP) goto pipe_closed; xassert(events & EPOLLOUT); ssize_t written = write(fd, &ctx->text[ctx->idx], ctx->left); if (written < 0) { LOG_WARN("failed to write to pipe: %s", strerror(errno)); goto pipe_closed; } xassert(written <= ctx->left); ctx->idx += written; ctx->left -= written; if (ctx->left == 0) goto pipe_closed; return true; pipe_closed: free(ctx->text); free(ctx); fdm_del(fdm, fd); return true; } static void alternate_scroll(struct seat *seat, int amount, int button); static bool execute_binding(struct seat *seat, struct terminal *term, const struct key_binding *binding, uint32_t serial, int amount) { const enum bind_action_normal action = binding->action; switch (action) { case BIND_ACTION_NONE: return true; case BIND_ACTION_NOOP: return true; case BIND_ACTION_SCROLLBACK_UP_PAGE: if (term->grid == &term->normal) { cmd_scrollback_up(term, term->rows); return true; } break; case BIND_ACTION_SCROLLBACK_UP_HALF_PAGE: if (term->grid == &term->normal) { cmd_scrollback_up(term, max(term->rows / 2, 1)); return true; } break; case BIND_ACTION_SCROLLBACK_UP_LINE: if (term->grid == &term->normal) { cmd_scrollback_up(term, 1); return true; } break; case BIND_ACTION_SCROLLBACK_UP_MOUSE: if (term->grid == &term->alt) { if (term->alt_scrolling) alternate_scroll(seat, amount, BTN_BACK); } else cmd_scrollback_up(term, amount); break; case BIND_ACTION_SCROLLBACK_DOWN_PAGE: if (term->grid == &term->normal) { cmd_scrollback_down(term, term->rows); return true; } break; case BIND_ACTION_SCROLLBACK_DOWN_HALF_PAGE: if (term->grid == &term->normal) { cmd_scrollback_down(term, max(term->rows / 2, 1)); return true; } break; case BIND_ACTION_SCROLLBACK_DOWN_LINE: if (term->grid == &term->normal) { cmd_scrollback_down(term, 1); return true; } break; case BIND_ACTION_SCROLLBACK_DOWN_MOUSE: if (term->grid == &term->alt) { if (term->alt_scrolling) alternate_scroll(seat, amount, BTN_FORWARD); } else cmd_scrollback_down(term, amount); break; case BIND_ACTION_SCROLLBACK_HOME: if (term->grid == &term->normal) { cmd_scrollback_up(term, term->grid->num_rows); return true; } break; case BIND_ACTION_SCROLLBACK_END: if (term->grid == &term->normal) { cmd_scrollback_down(term, term->grid->num_rows); return true; } break; case BIND_ACTION_CLIPBOARD_COPY: selection_to_clipboard(seat, term, serial); return true; case BIND_ACTION_CLIPBOARD_PASTE: selection_from_clipboard(seat, term, serial); term_reset_view(term); return true; case BIND_ACTION_PRIMARY_PASTE: selection_from_primary(seat, term); term_reset_view(term); return true; case BIND_ACTION_SEARCH_START: search_begin(term); return true; case BIND_ACTION_FONT_SIZE_UP: term_font_size_increase(term); return true; case BIND_ACTION_FONT_SIZE_DOWN: term_font_size_decrease(term); return true; case BIND_ACTION_FONT_SIZE_RESET: term_font_size_reset(term); return true; case BIND_ACTION_SPAWN_TERMINAL: term_spawn_new(term); return true; case BIND_ACTION_MINIMIZE: xdg_toplevel_set_minimized(term->window->xdg_toplevel); return true; case BIND_ACTION_MAXIMIZE: if (term->window->is_fullscreen) xdg_toplevel_unset_fullscreen(term->window->xdg_toplevel); if (term->window->is_maximized) xdg_toplevel_unset_maximized(term->window->xdg_toplevel); else xdg_toplevel_set_maximized(term->window->xdg_toplevel); return true; case BIND_ACTION_FULLSCREEN: if (term->window->is_fullscreen) xdg_toplevel_unset_fullscreen(term->window->xdg_toplevel); else xdg_toplevel_set_fullscreen(term->window->xdg_toplevel, NULL); return true; case BIND_ACTION_PIPE_SCROLLBACK: if (term->grid == &term->alt) break; /* FALLTHROUGH */ case BIND_ACTION_PIPE_VIEW: case BIND_ACTION_PIPE_SELECTED: case BIND_ACTION_PIPE_COMMAND_OUTPUT: { if (binding->aux->type != BINDING_AUX_PIPE) return true; struct pipe_context *ctx = NULL; int pipe_fd[2] = {-1, -1}; int stdout_fd = -1; int stderr_fd = -1; char *text = NULL; size_t len = 0; if (pipe(pipe_fd) < 0) { LOG_ERRNO("failed to create pipe"); goto pipe_err; } stdout_fd = open("/dev/null", O_WRONLY); stderr_fd = open("/dev/null", O_WRONLY); if (stdout_fd < 0 || stderr_fd < 0) { LOG_ERRNO("failed to open /dev/null"); goto pipe_err; } bool success; switch (action) { case BIND_ACTION_PIPE_SCROLLBACK: success = term_scrollback_to_text(term, &text, &len); break; case BIND_ACTION_PIPE_VIEW: success = term_view_to_text(term, &text, &len); break; case BIND_ACTION_PIPE_SELECTED: text = selection_to_text(term); success = text != NULL; len = text != NULL ? strlen(text) : 0; break; case BIND_ACTION_PIPE_COMMAND_OUTPUT: success = term_command_output_to_text(term, &text, &len); break; default: BUG("Unhandled action type"); success = false; break; } if (!success) goto pipe_err; /* Make write-end non-blocking; required by the FDM */ { int flags = fcntl(pipe_fd[1], F_GETFL); if (flags < 0 || fcntl(pipe_fd[1], F_SETFL, flags | O_NONBLOCK) < 0) { LOG_ERRNO("failed to make write-end of pipe non-blocking"); goto pipe_err; } } /* Make sure write-end is closed on exec() - or the spawned * program may not terminate*/ { int flags = fcntl(pipe_fd[1], F_GETFD); if (flags < 0 || fcntl(pipe_fd[1], F_SETFD, flags | FD_CLOEXEC) < 0) { LOG_ERRNO("failed to set FD_CLOEXEC on writeend of pipe"); goto pipe_err; } } if (spawn(term->reaper, term->cwd, binding->aux->pipe.args, pipe_fd[0], stdout_fd, stderr_fd, NULL, NULL, NULL) < 0) goto pipe_err; /* Close read end */ close(pipe_fd[0]); ctx = xmalloc(sizeof(*ctx)); *ctx = (struct pipe_context){ .text = text, .left = len, }; /* Asynchronously write the output to the pipe */ if (!fdm_add(term->fdm, pipe_fd[1], EPOLLOUT, &fdm_write_pipe, ctx)) goto pipe_err; return true; pipe_err: if (stdout_fd >= 0) close(stdout_fd); if (stderr_fd >= 0) close(stderr_fd); if (pipe_fd[0] >= 0) close(pipe_fd[0]); if (pipe_fd[1] >= 0) close(pipe_fd[1]); free(text); free(ctx); return true; } case BIND_ACTION_SHOW_URLS_COPY: case BIND_ACTION_SHOW_URLS_LAUNCH: case BIND_ACTION_SHOW_URLS_PERSISTENT: { xassert(!urls_mode_is_active(term)); enum url_action url_action = action == BIND_ACTION_SHOW_URLS_COPY ? URL_ACTION_COPY : action == BIND_ACTION_SHOW_URLS_LAUNCH ? URL_ACTION_LAUNCH : URL_ACTION_PERSISTENT; urls_collect(term, url_action, &term->conf->url.preg, true, &term->urls); urls_assign_key_combos(term->conf, &term->urls); urls_render(term, &term->conf->url.launch); return true; } case BIND_ACTION_TEXT_BINDING: xassert(binding->aux->type == BINDING_AUX_TEXT); term_to_slave(term, binding->aux->text.data, binding->aux->text.len); return true; case BIND_ACTION_PROMPT_PREV: { if (term->grid != &term->normal) return false; struct grid *grid = term->grid; const int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); /* Check each row from current view-1 (that is, the first * currently not visible row), up to, and including, the * scrollback start */ for (int r_sb_rel = grid_row_abs_to_sb_precalc_sb_start( grid, sb_start, grid->view) - 1; r_sb_rel >= 0; r_sb_rel--) { const int r_abs = grid_row_sb_to_abs_precalc_sb_start(grid, sb_start, r_sb_rel); const struct row *row = grid->rows[r_abs]; xassert(row != NULL); if (!row->shell_integration.prompt_marker) continue; grid->view = r_abs; term_damage_view(term); render_refresh(term); break; } return true; } case BIND_ACTION_PROMPT_NEXT: { if (term->grid != &term->normal) return false; struct grid *grid = term->grid; const int num_rows = grid->num_rows; if (grid->view == grid->offset) { /* Already at the bottom */ return true; } /* Check each row from view+1, to the bottom of the scrollback */ for (int r_abs = (grid->view + 1) & (num_rows - 1); ; r_abs = (r_abs + 1) & (num_rows - 1)) { const struct row *row = grid->rows[r_abs]; xassert(row != NULL); if (!row->shell_integration.prompt_marker) { if (r_abs == grid->offset + term->rows - 1) { /* We've reached the bottom of the scrollback */ break; } continue; } int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); int ofs_sb_rel = grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, grid->offset); int new_view_sb_rel = grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, r_abs); new_view_sb_rel = min(ofs_sb_rel, new_view_sb_rel); grid->view = grid_row_sb_to_abs_precalc_sb_start( grid, sb_start, new_view_sb_rel); term_damage_view(term); render_refresh(term); break; } return true; } case BIND_ACTION_UNICODE_INPUT: unicode_mode_activate(term); return true; case BIND_ACTION_QUIT: term_shutdown(term); return true; case BIND_ACTION_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_SELECT_BEGIN: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, false); return true; case BIND_ACTION_SELECT_BEGIN_BLOCK: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_BLOCK, false); return true; case BIND_ACTION_SELECT_EXTEND: selection_extend( seat, term, seat->mouse.col, seat->mouse.row, term->selection.kind); return true; case BIND_ACTION_SELECT_EXTEND_CHAR_WISE: if (term->selection.kind != SELECTION_BLOCK) { selection_extend( seat, term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE); return true; } return false; case BIND_ACTION_SELECT_WORD: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE, false); return true; case BIND_ACTION_SELECT_WORD_WS: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE, true); return true; case BIND_ACTION_SELECT_QUOTE: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_QUOTE_WISE, false); break; case BIND_ACTION_SELECT_ROW: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_LINE_WISE, false); return true; case BIND_ACTION_COUNT: BUG("Invalid action type"); return false; } return false; } static void keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, uint32_t format, int32_t fd, uint32_t size) { LOG_DBG("keyboard_keymap: keyboard=%p (format=%u, size=%u)", (void *)wl_keyboard, format, size); struct seat *seat = data; struct wayland *wayl = seat->wayl; /* * Free old keymap state */ if (seat->kbd.xkb_keymap != NULL) { xkb_keymap_unref(seat->kbd.xkb_keymap); seat->kbd.xkb_keymap = NULL; } if (seat->kbd.xkb_state != NULL) { xkb_state_unref(seat->kbd.xkb_state); seat->kbd.xkb_state = NULL; } key_binding_unload_keymap(wayl->key_binding_manager, seat); /* Verify keymap is in a format we understand */ switch ((enum wl_keyboard_keymap_format)format) { case WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP: close(fd); return; case WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1: break; default: LOG_WARN("unrecognized keymap format: %u", format); close(fd); return; } 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; } while (map_str[size - 1] == '\0') size--; if (seat->kbd.xkb != NULL) { seat->kbd.xkb_keymap = xkb_keymap_new_from_buffer( seat->kbd.xkb, map_str, size, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); } if (seat->kbd.xkb_keymap != NULL) { seat->kbd.xkb_state = xkb_state_new(seat->kbd.xkb_keymap); seat->kbd.mod_shift = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); seat->kbd.mod_alt = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; seat->kbd.mod_ctrl = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_CTRL); seat->kbd.mod_super = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_LOGO); seat->kbd.mod_caps = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_CAPS); seat->kbd.mod_num = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_NUM); /* Significant modifiers in the legacy keyboard protocol */ seat->kbd.legacy_significant = 0; if (seat->kbd.mod_shift != XKB_MOD_INVALID) seat->kbd.legacy_significant |= 1 << seat->kbd.mod_shift; if (seat->kbd.mod_alt != XKB_MOD_INVALID) seat->kbd.legacy_significant |= 1 << seat->kbd.mod_alt; if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) seat->kbd.legacy_significant |= 1 << seat->kbd.mod_ctrl; if (seat->kbd.mod_super != XKB_MOD_INVALID) seat->kbd.legacy_significant |= 1 << seat->kbd.mod_super; /* Significant modifiers in the kitty keyboard protocol */ seat->kbd.kitty_significant = seat->kbd.legacy_significant; if (seat->kbd.mod_caps != XKB_MOD_INVALID) seat->kbd.kitty_significant |= 1 << seat->kbd.mod_caps; if (seat->kbd.mod_num != XKB_MOD_INVALID) seat->kbd.kitty_significant |= 1 << seat->kbd.mod_num; 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); } static void keyboard_enter(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, struct wl_surface *surface, struct wl_array *keys) { xassert(surface != NULL); xassert(serial != 0); struct seat *seat = data; struct wl_window *win = wl_surface_get_user_data(surface); struct terminal *term = win->term; LOG_DBG("%s: keyboard_enter: keyboard=%p, serial=%u, surface=%p", seat->name, (void *)wl_keyboard, serial, (void *)surface); term_kbd_focus_in(term); seat->kbd_focus = term; seat->kbd.serial = serial; } static bool start_repeater(struct seat *seat, uint32_t key) { if (seat->kbd.repeat.dont_re_repeat) return true; if (seat->kbd.repeat.rate == 0) return true; struct itimerspec t = { .it_value = {.tv_sec = 0, .tv_nsec = seat->kbd.repeat.delay * 1000000}, .it_interval = {.tv_sec = 0, .tv_nsec = 1000000000 / seat->kbd.repeat.rate}, }; if (t.it_value.tv_nsec >= 1000000000) { t.it_value.tv_sec += t.it_value.tv_nsec / 1000000000; t.it_value.tv_nsec %= 1000000000; } if (t.it_interval.tv_nsec >= 1000000000) { t.it_interval.tv_sec += t.it_interval.tv_nsec / 1000000000; t.it_interval.tv_nsec %= 1000000000; } if (timerfd_settime(seat->kbd.repeat.fd, 0, &t, NULL) < 0) { LOG_ERRNO("%s: failed to arm keyboard repeat timer", seat->name); return false; } seat->kbd.repeat.key = key; return true; } static bool stop_repeater(struct seat *seat, uint32_t key) { if (key != -1 && key != seat->kbd.repeat.key) return true; if (timerfd_settime(seat->kbd.repeat.fd, 0, &(struct itimerspec){{0}}, NULL) < 0) { LOG_ERRNO("%s: failed to disarm keyboard repeat timer", seat->name); return false; } return true; } static void keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, struct wl_surface *surface) { struct seat *seat = data; LOG_DBG("keyboard_leave: keyboard=%p, serial=%u, surface=%p", (void *)wl_keyboard, serial, (void *)surface); xassert( seat->kbd_focus == NULL || surface == NULL || /* Seen on Sway 1.2 */ ((const struct wl_window *)wl_surface_get_user_data(surface))->term == seat->kbd_focus ); struct terminal *old_focused = seat->kbd_focus; seat->kbd_focus = NULL; stop_repeater(seat, -1); seat->kbd.shift = false; seat->kbd.alt = false; seat->kbd.ctrl = false; seat->kbd.super = false; if (seat->kbd.xkb_compose_state != NULL) xkb_compose_state_reset(seat->kbd.xkb_compose_state); if (old_focused != NULL) { seat->pointer.hidden = false; term_xcursor_update_for_seat(old_focused, seat); term_kbd_focus_out(old_focused); } else { /* * Sway bug - under certain conditions we get a * keyboard_leave() (and keyboard_key()) without first having * received a keyboard_enter() */ LOG_WARN( "compositor sent keyboard_leave event without a keyboard_enter " "event: surface=%p", (void *)surface); } } static const struct key_data * keymap_data_for_sym(xkb_keysym_t sym, size_t *count) { switch (sym) { case XKB_KEY_Escape: *count = ALEN(key_escape); return key_escape; case XKB_KEY_Return: *count = ALEN(key_return); return key_return; case XKB_KEY_ISO_Left_Tab: *count = ALEN(key_iso_left_tab); return key_iso_left_tab; case XKB_KEY_Tab: *count = ALEN(key_tab); return key_tab; case XKB_KEY_BackSpace: *count = ALEN(key_backspace); return key_backspace; case XKB_KEY_Up: *count = ALEN(key_up); return key_up; case XKB_KEY_Down: *count = ALEN(key_down); return key_down; case XKB_KEY_Right: *count = ALEN(key_right); return key_right; case XKB_KEY_Left: *count = ALEN(key_left); return key_left; case XKB_KEY_Home: *count = ALEN(key_home); return key_home; case XKB_KEY_End: *count = ALEN(key_end); return key_end; case XKB_KEY_Insert: *count = ALEN(key_insert); return key_insert; case XKB_KEY_Delete: *count = ALEN(key_delete); return key_delete; case XKB_KEY_Page_Up: *count = ALEN(key_pageup); return key_pageup; case XKB_KEY_Page_Down: *count = ALEN(key_pagedown); return key_pagedown; case XKB_KEY_F1: *count = ALEN(key_f1); return key_f1; case XKB_KEY_F2: *count = ALEN(key_f2); return key_f2; case XKB_KEY_F3: *count = ALEN(key_f3); return key_f3; case XKB_KEY_F4: *count = ALEN(key_f4); return key_f4; case XKB_KEY_F5: *count = ALEN(key_f5); return key_f5; case XKB_KEY_F6: *count = ALEN(key_f6); return key_f6; case XKB_KEY_F7: *count = ALEN(key_f7); return key_f7; case XKB_KEY_F8: *count = ALEN(key_f8); return key_f8; case XKB_KEY_F9: *count = ALEN(key_f9); return key_f9; case XKB_KEY_F10: *count = ALEN(key_f10); return key_f10; case XKB_KEY_F11: *count = ALEN(key_f11); return key_f11; case XKB_KEY_F12: *count = ALEN(key_f12); return key_f12; case XKB_KEY_F13: *count = ALEN(key_f13); return key_f13; case XKB_KEY_F14: *count = ALEN(key_f14); return key_f14; case XKB_KEY_F15: *count = ALEN(key_f15); return key_f15; case XKB_KEY_F16: *count = ALEN(key_f16); return key_f16; case XKB_KEY_F17: *count = ALEN(key_f17); return key_f17; case XKB_KEY_F18: *count = ALEN(key_f18); return key_f18; case XKB_KEY_F19: *count = ALEN(key_f19); return key_f19; case XKB_KEY_F20: *count = ALEN(key_f20); return key_f20; case XKB_KEY_F21: *count = ALEN(key_f21); return key_f21; case XKB_KEY_F22: *count = ALEN(key_f22); return key_f22; case XKB_KEY_F23: *count = ALEN(key_f23); return key_f23; case XKB_KEY_F24: *count = ALEN(key_f24); return key_f24; case XKB_KEY_F25: *count = ALEN(key_f25); return key_f25; case XKB_KEY_F26: *count = ALEN(key_f26); return key_f26; case XKB_KEY_F27: *count = ALEN(key_f27); return key_f27; case XKB_KEY_F28: *count = ALEN(key_f28); return key_f28; case XKB_KEY_F29: *count = ALEN(key_f29); return key_f29; case XKB_KEY_F30: *count = ALEN(key_f30); return key_f30; case XKB_KEY_F31: *count = ALEN(key_f31); return key_f31; case XKB_KEY_F32: *count = ALEN(key_f32); return key_f32; case XKB_KEY_F33: *count = ALEN(key_f33); return key_f33; case XKB_KEY_F34: *count = ALEN(key_f34); return key_f34; case XKB_KEY_F35: *count = ALEN(key_f35); return key_f35; case XKB_KEY_KP_Up: *count = ALEN(key_kp_up); return key_kp_up; case XKB_KEY_KP_Down: *count = ALEN(key_kp_down); return key_kp_down; case XKB_KEY_KP_Right: *count = ALEN(key_kp_right); return key_kp_right; case XKB_KEY_KP_Left: *count = ALEN(key_kp_left); return key_kp_left; case XKB_KEY_KP_Begin: *count = ALEN(key_kp_begin); return key_kp_begin; case XKB_KEY_KP_Home: *count = ALEN(key_kp_home); return key_kp_home; case XKB_KEY_KP_End: *count = ALEN(key_kp_end); return key_kp_end; case XKB_KEY_KP_Insert: *count = ALEN(key_kp_insert); return key_kp_insert; case XKB_KEY_KP_Delete: *count = ALEN(key_kp_delete); return key_kp_delete; case XKB_KEY_KP_Page_Up: *count = ALEN(key_kp_pageup); return key_kp_pageup; case XKB_KEY_KP_Page_Down: *count = ALEN(key_kp_pagedown); return key_kp_pagedown; case XKB_KEY_KP_Enter: *count = ALEN(key_kp_enter); return key_kp_enter; case XKB_KEY_KP_Divide: *count = ALEN(key_kp_divide); return key_kp_divide; case XKB_KEY_KP_Multiply: *count = ALEN(key_kp_multiply); return key_kp_multiply; case XKB_KEY_KP_Subtract: *count = ALEN(key_kp_subtract); return key_kp_subtract; case XKB_KEY_KP_Add: *count = ALEN(key_kp_add); return key_kp_add; case XKB_KEY_KP_Separator: *count = ALEN(key_kp_separator); return key_kp_separator; case XKB_KEY_KP_Decimal: *count = ALEN(key_kp_decimal); return key_kp_decimal; case XKB_KEY_KP_0: *count = ALEN(key_kp_0); return key_kp_0; case XKB_KEY_KP_1: *count = ALEN(key_kp_1); return key_kp_1; case XKB_KEY_KP_2: *count = ALEN(key_kp_2); return key_kp_2; case XKB_KEY_KP_3: *count = ALEN(key_kp_3); return key_kp_3; case XKB_KEY_KP_4: *count = ALEN(key_kp_4); return key_kp_4; case XKB_KEY_KP_5: *count = ALEN(key_kp_5); return key_kp_5; case XKB_KEY_KP_6: *count = ALEN(key_kp_6); return key_kp_6; case XKB_KEY_KP_7: *count = ALEN(key_kp_7); return key_kp_7; case XKB_KEY_KP_8: *count = ALEN(key_kp_8); return key_kp_8; case XKB_KEY_KP_9: *count = ALEN(key_kp_9); return key_kp_9; } return NULL; } static const struct key_data * keymap_lookup(struct terminal *term, xkb_keysym_t sym, enum modifier mods) { size_t count; const struct key_data *info = keymap_data_for_sym(sym, &count); if (info == NULL) return NULL; const enum cursor_keys cursor_keys_mode = term->cursor_keys_mode; const enum keypad_keys keypad_keys_mode = term->num_lock_modifier ? KEYPAD_NUMERICAL : term->keypad_keys_mode; LOG_DBG("keypad mode: %d", keypad_keys_mode); for (size_t j = 0; j < count; j++) { enum modifier modifiers = info[j].modifiers; if (modifiers & MOD_MODIFY_OTHER_KEYS_STATE1) { if (term->modify_other_keys_2) continue; modifiers &= ~MOD_MODIFY_OTHER_KEYS_STATE1; } if (modifiers & MOD_MODIFY_OTHER_KEYS_STATE2) { if (!term->modify_other_keys_2) continue; modifiers &= ~MOD_MODIFY_OTHER_KEYS_STATE2; } if (modifiers != MOD_ANY && modifiers != mods) continue; if (info[j].cursor_keys_mode != CURSOR_KEYS_DONTCARE && info[j].cursor_keys_mode != cursor_keys_mode) continue; if (info[j].keypad_keys_mode != KEYPAD_DONTCARE && info[j].keypad_keys_mode != keypad_keys_mode) continue; return &info[j]; } return NULL; } UNITTEST { struct terminal term = { .num_lock_modifier = false, .keypad_keys_mode = KEYPAD_NUMERICAL, .cursor_keys_mode = CURSOR_KEYS_NORMAL, }; const struct key_data *info = keymap_lookup(&term, XKB_KEY_ISO_Left_Tab, MOD_SHIFT | MOD_CTRL); xassert(info != NULL); xassert(streq(info->seq, "\033[27;6;9~")); } UNITTEST { struct terminal term = { .modify_other_keys_2 = false, }; const struct key_data *info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT); xassert(info != NULL); xassert(streq(info->seq, "\033\r")); term.modify_other_keys_2 = true; info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT); xassert(info != NULL); xassert(streq(info->seq, "\033[27;3;13~")); } void get_current_modifiers(const struct seat *seat, xkb_mod_mask_t *effective, xkb_mod_mask_t *consumed, uint32_t key, bool filter_locked) { if (unlikely(seat->kbd.xkb_state == NULL)) { if (effective != NULL) *effective = 0; if (consumed != NULL) *consumed = 0; } else { const xkb_mod_mask_t locked = xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); if (effective != NULL) { *effective = xkb_state_serialize_mods( seat->kbd.xkb_state, XKB_STATE_MODS_EFFECTIVE); if (filter_locked) *effective &= ~locked; } if (consumed != NULL) { *consumed = xkb_state_key_get_consumed_mods2( seat->kbd.xkb_state, key, XKB_CONSUMED_MODE_XKB); if (filter_locked) *consumed &= ~locked; } } } struct kbd_ctx { xkb_layout_index_t layout; xkb_keycode_t key; xkb_keysym_t sym; struct { const xkb_keysym_t *syms; size_t count; } level0_syms; xkb_mod_mask_t mods; xkb_mod_mask_t consumed; struct { const uint8_t *buf; size_t count; } utf8; uint32_t *utf32; enum xkb_compose_status compose_status; enum wl_keyboard_key_state key_state; }; static bool legacy_kbd_protocol(struct seat *seat, struct terminal *term, const struct kbd_ctx *ctx) { if (ctx->key_state != WL_KEYBOARD_KEY_STATE_PRESSED) return false; if (ctx->compose_status == XKB_COMPOSE_COMPOSING) return false; enum modifier keymap_mods = MOD_NONE; keymap_mods |= seat->kbd.shift ? MOD_SHIFT : MOD_NONE; keymap_mods |= seat->kbd.alt ? MOD_ALT : MOD_NONE; keymap_mods |= seat->kbd.ctrl ? MOD_CTRL : MOD_NONE; keymap_mods |= seat->kbd.super ? MOD_META : MOD_NONE; const xkb_keysym_t sym = ctx->sym; const size_t count = ctx->utf8.count; const uint8_t *const utf8 = ctx->utf8.buf; const struct key_data *keymap = keymap_lookup(term, sym, keymap_mods); if (keymap != NULL) { term_to_slave(term, keymap->seq, strlen(keymap->seq)); return true; } if (count == 0) return false; #define is_control_key(x) ((x) >= 0x40 && (x) <= 0x7f) #define IS_CTRL(x) ((x) < 0x20 || ((x) >= 0x7f && (x) <= 0x9f)) //LOG_DBG("term->modify_other_keys=%d, count=%zu, is_ctrl=%d (utf8=0x%02x), sym=%d", //term->modify_other_keys_2, count, IS_CTRL(utf8[0]), utf8[0], sym); bool ctrl_is_in_effect = (keymap_mods & MOD_CTRL) != 0; bool ctrl_seq = is_control_key(sym) || (count == 1 && IS_CTRL(utf8[0])); bool modify_other_keys2_in_effect = false; if (term->modify_other_keys_2) { /* * Try to mimic XTerm's behavior, when holding shift: * * - if other modifiers are pressed (e.g. Alt), emit a CSI escape * - upper-case symbols A-Z are encoded as an CSI escape * - other upper-case symbols (e.g 'Ö') or emitted as is * - non-upper cased symbols are _mostly_ emitted as is (foot * always emits as is) * * Examples (assuming Swedish layout): * - Shift-a ('A') emits a CSI * - Shift-, (';') emits ';' * - Shift-Alt-, (Alt-;) emits a CSI * - Shift-ö ('Ö') emits 'Ö' */ /* Any modifiers, besides shift active? */ const xkb_mod_mask_t shift_mask = 1 << seat->kbd.mod_shift; if ((ctx->mods & ~shift_mask & seat->kbd.legacy_significant) != 0) modify_other_keys2_in_effect = true; else { const xkb_layout_index_t layout_idx = xkb_state_key_get_layout( seat->kbd.xkb_state, ctx->key); /* * Get pressed key's base symbol. * - for 'A' (shift-a), that's 'a' * - for ';' (shift-,), that's ',' */ const xkb_keysym_t *base_syms = NULL; size_t base_count = xkb_keymap_key_get_syms_by_level( seat->kbd.xkb_keymap, ctx->key, layout_idx, 0, &base_syms); /* Check if base symbol(s) is a-z. If so, emit CSI */ const xkb_keysym_t lower_cased_sym = xkb_keysym_to_lower(ctx->sym); for (size_t i = 0; i < base_count; i++) { const xkb_keysym_t s = base_syms[i]; if (lower_cased_sym == s && s >= XKB_KEY_a && s <= XKB_KEY_z) { modify_other_keys2_in_effect = true; break; } } } } if (keymap_mods != MOD_NONE && (modify_other_keys2_in_effect || (ctrl_is_in_effect && !ctrl_seq))) { static const int mod_param_map[32] = { [MOD_SHIFT] = 2, [MOD_ALT] = 3, [MOD_SHIFT | MOD_ALT] = 4, [MOD_CTRL] = 5, [MOD_SHIFT | MOD_CTRL] = 6, [MOD_ALT | MOD_CTRL] = 7, [MOD_SHIFT | MOD_ALT | MOD_CTRL] = 8, [MOD_META] = 9, [MOD_META | MOD_SHIFT] = 10, [MOD_META | MOD_ALT] = 11, [MOD_META | MOD_SHIFT | MOD_ALT] = 12, [MOD_META | MOD_CTRL] = 13, [MOD_META | MOD_SHIFT | MOD_CTRL] = 14, [MOD_META | MOD_ALT | MOD_CTRL] = 15, [MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL] = 16, }; xassert(keymap_mods < ALEN(mod_param_map)); int modify_param = mod_param_map[keymap_mods]; xassert(modify_param != 0); char reply[32]; size_t n = xsnprintf(reply, sizeof(reply), "\x1b[27;%d;%d~", modify_param, sym); term_to_slave(term, reply, n); } else if (keymap_mods & MOD_ALT) { /* * When the alt modifier is pressed, we do one out of three things: * * 1. we prefix the output bytes with ESC * 2. we set the 8:th bit in the output byte * 3. we ignore the alt modifier * * #1 is configured with \E[?1036, and is on by default * * If #1 has been disabled, we use #2, *if* it's a single byte * we're emitting. Since this is a UTF-8 terminal, we then * UTF8-encode the 8-bit character. #2 is configured with * \E[?1034, and is on by default. * * Lastly, if both #1 and #2 have been disabled, the alt * modifier is ignored. */ if (term->meta.esc_prefix) { term_to_slave(term, "\x1b", 1); term_to_slave(term, utf8, count); } else if (term->meta.eight_bit && count == 1) { const char32_t wc = 0x80 | utf8[0]; char utf8_meta[MB_CUR_MAX]; size_t chars = c32rtomb(utf8_meta, wc, &(mbstate_t){0}); if (chars != (size_t)-1) term_to_slave(term, utf8_meta, chars); else term_to_slave(term, utf8, count); } else { /* Alt ignored */ term_to_slave(term, utf8, count); } } else term_to_slave(term, utf8, count); return true; } UNITTEST { /* Verify the kitty keymap is sorted */ xkb_keysym_t last = 0; for (size_t i = 0; i < ALEN(kitty_keymap); i++) { const struct kitty_key_data *e = &kitty_keymap[i]; xassert(e->sym > last); last = e->sym; } } static int kitty_search(const void *_key, const void *_e) { const xkb_keysym_t *key = _key; const struct kitty_key_data *e = _e; return *key - e->sym; } static bool kitty_kbd_protocol(struct seat *seat, struct terminal *term, const struct kbd_ctx *ctx) { const bool repeating = seat->kbd.repeat.dont_re_repeat; const bool pressed = ctx->key_state == WL_KEYBOARD_KEY_STATE_PRESSED && !repeating; const bool released = ctx->key_state == WL_KEYBOARD_KEY_STATE_RELEASED; const bool composing = ctx->compose_status == XKB_COMPOSE_COMPOSING; const bool composed = ctx->compose_status == XKB_COMPOSE_COMPOSED; const enum kitty_kbd_flags flags = term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx]; const bool disambiguate = flags & KITTY_KBD_DISAMBIGUATE; const bool report_events = flags & KITTY_KBD_REPORT_EVENT; const bool report_alternate = flags & KITTY_KBD_REPORT_ALTERNATE; const bool report_all_as_escapes = flags & KITTY_KBD_REPORT_ALL; if (!report_events && released) return false; /* TODO: should we even bother with this, or just say it's not supported? */ if (!disambiguate && !report_all_as_escapes && pressed) return legacy_kbd_protocol(seat, term, ctx); const xkb_keysym_t sym = ctx->sym; const uint32_t *utf32 = ctx->utf32; const uint8_t *const utf8 = ctx->utf8.buf; const size_t count = ctx->utf8.count; /* Lookup sym in the pre-defined keysym table */ const struct kitty_key_data *info = bsearch( &sym, kitty_keymap, ALEN(kitty_keymap), sizeof(kitty_keymap[0]), &kitty_search); xassert(info == NULL || info->sym == sym); xkb_mod_mask_t mods = 0; xkb_mod_mask_t locked = 0; xkb_mod_mask_t consumed = ctx->consumed; if (info != NULL && info->is_modifier) { /* * Special-case modifier keys. * * Normally, the "current" XKB state reflects the state * *before* the current key event. In other words, the * modifiers for key events that affect the modifier state * (e.g. one of the control keys, or shift keys etc) does * *not* include the key itself. * * Put another way, if you press "control", the modifier set * is empty in the key press event, but contains "ctrl" in the * release event. * * The kitty protocol mandates the modifier list contain the * key itself, in *both* the press and release event. * * We handle this by updating the XKB state to *include* the * current key, retrieve the set of modifiers (including the * set of consumed modifiers), and then revert the XKB update. */ xkb_state_update_key( seat->kbd.xkb_state, ctx->key, pressed ? XKB_KEY_DOWN : XKB_KEY_UP); get_current_modifiers(seat, &mods, NULL, 0, false); locked = xkb_state_serialize_mods( seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); consumed = xkb_state_key_get_consumed_mods2( seat->kbd.xkb_state, ctx->key, XKB_CONSUMED_MODE_XKB); #if 0 /* * TODO: according to the XKB docs, state updates should * always be in pairs: each press should be followed by a * release. However, doing this just breaks the xkb state. * * *Not* pairing the above press/release with a corresponding * release/press appears to do exactly what we want. */ xkb_state_update_key( seat->kbd.xkb_state, ctx->key, pressed ? XKB_KEY_UP : XKB_KEY_DOWN); #endif } else { /* Same as ctx->mods, but *without* filtering locked modifiers */ get_current_modifiers(seat, &mods, NULL, 0, false); locked = xkb_state_serialize_mods( seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); } mods &= seat->kbd.kitty_significant; consumed &= seat->kbd.kitty_significant; /* * A note on locked modifiers; they *are* a part of the protocol, * and *should* be included in the modifier set reported in the * key event. * * However, *only* if the key would result in a CSIu *without* the * locked modifier being enabled * * Translated: if *another* modifier is active, or if * report-all-keys-as-escapes is enabled, then we include the * locked modifier in the key event. * * But, if the key event would result in plain text output without * the locked modifier, then we "ignore" the locked modifier and * emit plain text anyway. */ bool is_text = count > 0 && utf32 != NULL && (mods & ~locked & ~consumed) == 0; for (size_t i = 0; utf32[i] != U'\0'; i++) { if (!isc32print(utf32[i])) { is_text = false; break; } } const bool report_associated_text = (flags & KITTY_KBD_REPORT_ASSOCIATED) && is_text && !released; if (composing) { /* We never emit anything while composing, *except* modifiers * (and only in report-all-keys-as-escape-codes mode) */ if (info != NULL && info->is_modifier) goto emit_escapes; return false; } if (report_all_as_escapes) goto emit_escapes; if ((mods & ~locked & ~consumed) == 0) { switch (sym) { case XKB_KEY_Return: if (!released) term_to_slave(term, "\r", 1); return true; case XKB_KEY_BackSpace: if (!released) term_to_slave(term, "\x7f", 1); return true; case XKB_KEY_Tab: if (!released) term_to_slave(term, "\t", 1); return true; } } /* Plain-text without modifiers, or commposed text, is emitted as-is */ if (is_text && !released) { term_to_slave(term, utf8, count); return true; } emit_escapes: ; unsigned int encoded_mods = 0; if (seat->kbd.mod_shift != XKB_MOD_INVALID) encoded_mods |= mods & (1 << seat->kbd.mod_shift) ? (1 << 0) : 0; if (seat->kbd.mod_alt != XKB_MOD_INVALID) encoded_mods |= mods & (1 << seat->kbd.mod_alt) ? (1 << 1) : 0; if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) encoded_mods |= mods & (1 << seat->kbd.mod_ctrl) ? (1 << 2) : 0; if (seat->kbd.mod_super != XKB_MOD_INVALID) encoded_mods |= mods & (1 << seat->kbd.mod_super) ? (1 << 3) : 0; if (seat->kbd.mod_caps != XKB_MOD_INVALID) encoded_mods |= mods & (1 << seat->kbd.mod_caps) ? (1 << 6) : 0; if (seat->kbd.mod_num != XKB_MOD_INVALID) encoded_mods |= mods & (1 << seat->kbd.mod_num) ? (1 << 7) : 0; encoded_mods++; /* * Figure out the main, alternate and base key codes. * * The main key is the unshifted version of the generated symbol, * the alternate key is the shifted version, and base is the * (unshifted) key assuming the default layout. * * For example, the user presses shift+a, then: * - unshifted = 'a' * - shifted = 'A' * - base = 'a' * * Base will in many cases be the same as the unshifted key, but * may differ if the active keyboard layout is non-ASCII (examples * would be russian, or alternative layouts like neo etc). * * The shifted key is what we get from XKB, i.e. the resulting key * from all active modifiers, plus the pressed key. */ int unshifted = -1, shifted = -1, base = -1; char final; if (info != NULL) { /* Use code from lookup table (cursor keys, enter, tab etc)*/ if (!info->is_modifier || report_all_as_escapes) { shifted = info->key; final = info->final; } } else { /* Use keysym (typically its Unicode codepoint value) */ if (composed) shifted = utf32[0]; /* TODO: what if there are multiple codepoints? */ else shifted = xkb_keysym_to_utf32(sym); final = 'u'; } if (shifted <= 0) return false; /* Base layout key. I.e the symbol the pressed key produces in * the base/default layout (layout idx 0) */ const xkb_keysym_t *base_syms; int base_sym_count = xkb_keymap_key_get_syms_by_level( seat->kbd.xkb_keymap, ctx->key, 0, 0, &base_syms); if (base_sym_count > 0) base = xkb_keysym_to_utf32(base_syms[0]); /* * If the keysym is shifted, use its unshifted codepoint * instead. In other words, ctrl+a and ctrl+shift+a should both * use the same value for 'key' (97 - i.a. 'a'). * * However, don't do this if a non-significant modifier was used * to generate the symbol. This is needed since we cannot encode * non-significant modifiers, and thus the "extra" modifier(s) * would get lost. * * Example: * * the Swedish layout has '2', QUOTATION MARK ("double quote"), * '@', and '²' on the same key. '2' is the base symbol. * * Shift+2 results in QUOTATION MARK * AltGr+2 results in '@' * AltGr+Shift+2 results in '²' * * The kitty kbd protocol can't encode AltGr. So, if we always * used the base symbol ('2'), Alt+Shift+2 would result in the * same escape sequence as AltGr+Alt+Shift+2. * * (yes, this matches what kitty does, as of 0.23.1) */ const bool use_level0_sym = (ctx->mods & ~seat->kbd.kitty_significant) == 0 && ctx->level0_syms.count > 0; unshifted = use_level0_sym ? xkb_keysym_to_utf32(ctx->level0_syms.syms[0]) : 0; xassert(encoded_mods >= 1); char event[4]; if (report_events /*&& !pressed*/) { /* Note: this deviates slightly from Kitty, which omits the * ":1" subparameter for key press events */ event[0] = ':'; event[1] = '0' + (pressed ? 1 : repeating ? 2 : 3); event[2] = '\0'; } else event[0] = '\0'; char buf[128], *p = buf; size_t left = sizeof(buf); size_t bytes; const int key = unshifted > 0 && isc32print(unshifted) && !composed ? unshifted : shifted; const int alternate = shifted; if (final == 'u' || final == '~') { bytes = snprintf(p, left, "\x1b[%u", key); p += bytes; left -= bytes; if (report_alternate) { bool emit_alternate = alternate > 0 && alternate != key; bool emit_base = base > 0 && base != key && base != alternate && isc32print(base); if (emit_alternate) { bytes = snprintf(p, left, ":%u", alternate); p += bytes; left -= bytes; } if (emit_base) { bytes = snprintf( p, left, "%s:%u", !emit_alternate ? ":" : "", base); p += bytes; left -= bytes; } } bool emit_mods = encoded_mods > 1 || event[0] != '\0'; if (emit_mods) { bytes = snprintf(p, left, ";%u%s", encoded_mods, event); p += bytes; left -= bytes; } if (report_associated_text) { bytes = snprintf(p, left, "%s;%u", !emit_mods ? ";" : "", utf32[0]); p += bytes; left -= bytes; /* Additional text codepoints */ if (utf32[0] != U'\0') { for (size_t i = 1; utf32[i] != U'\0'; i++) { bytes = snprintf(p, left, ":%u", utf32[i]); p += bytes; left -= bytes; } } } bytes = snprintf(p, left, "%c", final); p += bytes; left -= bytes; } else { if (encoded_mods > 1 || event[0] != '\0') { bytes = snprintf(p, left, "\x1b[1;%u%s%c", encoded_mods, event, final); p += bytes; left -= bytes; } else { bytes = snprintf(p, left, "\x1b[%c", final); p += bytes; left -= bytes; } } return term_to_slave(term, buf, sizeof(buf) - left); } /* Copied from libxkbcommon (internal function) */ static bool keysym_is_modifier(xkb_keysym_t keysym) { return (keysym >= XKB_KEY_Shift_L && keysym <= XKB_KEY_Hyper_R) || /* libX11 only goes up to XKB_KEY_ISO_Level5_Lock. */ (keysym >= XKB_KEY_ISO_Lock && keysym <= XKB_KEY_ISO_Last_Group_Lock) || keysym == XKB_KEY_Mode_switch || keysym == XKB_KEY_Num_Lock; } #if defined(_DEBUG) static void modifier_string(xkb_mod_mask_t mods, size_t sz, char mod_str[static sz], const struct seat *seat) { if (sz == 0) return; mod_str[0] = '\0'; for (size_t i = 0; i < sizeof(xkb_mod_mask_t) * 8; i++) { if (!(mods & (1u << i))) continue; strcat(mod_str, xkb_keymap_mod_get_name(seat->kbd.xkb_keymap, i)); strcat(mod_str, "+"); } if (mod_str[0] != '\0') { /* Strip the last '+' */ mod_str[strlen(mod_str) - 1] = '\0'; } if (mod_str[0] == '\0') { strcpy(mod_str, ""); } } #endif static void key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, uint32_t key, uint32_t state) { xassert(serial != 0); seat->kbd.serial = serial; if (seat->kbd.xkb == NULL || seat->kbd.xkb_keymap == NULL || seat->kbd.xkb_state == NULL) { return; } const bool pressed = state == WL_KEYBOARD_KEY_STATE_PRESSED; //const bool repeated = pressed && seat->kbd.repeat.dont_re_repeat; const bool released = state == WL_KEYBOARD_KEY_STATE_RELEASED; if (released) stop_repeater(seat, key); bool should_repeat = pressed && xkb_keymap_key_repeats(seat->kbd.xkb_keymap, key); xkb_keysym_t sym = xkb_state_key_get_one_sym(seat->kbd.xkb_state, key); if (pressed && term->conf->mouse.hide_when_typing && !keysym_is_modifier(sym)) { seat->pointer.hidden = true; term_xcursor_update_for_seat(term, seat); } enum xkb_compose_status compose_status = XKB_COMPOSE_NOTHING; if (seat->kbd.xkb_compose_state != NULL) { if (pressed) xkb_compose_state_feed(seat->kbd.xkb_compose_state, sym); compose_status = xkb_compose_state_get_status( seat->kbd.xkb_compose_state); } const bool composed = compose_status == XKB_COMPOSE_COMPOSED; xkb_mod_mask_t mods, consumed; get_current_modifiers(seat, &mods, &consumed, key, true); xkb_layout_index_t layout_idx = xkb_state_key_get_layout(seat->kbd.xkb_state, key); const xkb_keysym_t *raw_syms = NULL; size_t raw_count = xkb_keymap_key_get_syms_by_level( seat->kbd.xkb_keymap, key, layout_idx, 0, &raw_syms); const struct key_binding_set *bindings = key_binding_for( seat->wayl->key_binding_manager, term->conf, seat); xassert(bindings != NULL); if (pressed) { if (term->unicode_mode.active) { unicode_mode_input(seat, term, sym); return; } else if (term->is_searching) { if (should_repeat) start_repeater(seat, key); search_input( seat, term, bindings, key, sym, mods, consumed, raw_syms, raw_count, serial); return; } else if (urls_mode_is_active(term)) { if (should_repeat) start_repeater(seat, key); urls_input( seat, term, bindings, key, sym, mods, consumed, raw_syms, raw_count, serial); return; } } #if defined(_DEBUG) && defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG char sym_name[100]; xkb_keysym_get_name(sym, sym_name, sizeof(sym_name)); char active_mods_str[256] = {0}; char consumed_mods_str[256] = {0}; char locked_mods_str[256] = {0}; const xkb_mod_mask_t locked = xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); modifier_string(mods, sizeof(active_mods_str), active_mods_str, seat); modifier_string(consumed, sizeof(consumed_mods_str), consumed_mods_str, seat); modifier_string(locked, sizeof(locked_mods_str), locked_mods_str, seat); LOG_DBG("%s: %s (%u/0x%x), seat=%s, term=%p, serial=%u, " "mods=%s (0x%08x), consumed=%s (0x%08x), locked=%s (0x%08x), " "repeats=%d", pressed ? "pressed" : "released", sym_name, sym, sym, seat->name, (void *)term, serial, active_mods_str, mods, consumed_mods_str, consumed, locked_mods_str, locked, should_repeat); #endif /* * User configurable bindings */ if (pressed) { /* Match untranslated symbols */ tll_foreach(bindings->key, it) { const struct key_binding *bind = &it->item; if (bind->mods != mods || bind->mods == 0) continue; for (size_t i = 0; i < raw_count; i++) { if (bind->k.sym == raw_syms[i] && execute_binding(seat, term, bind, serial, 1)) { 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)) { 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)) { goto maybe_repeat; } } } } /* * Keys generating escape sequences */ /* * Compose, and maybe emit "normal" character */ xassert(seat->kbd.xkb_compose_state != NULL || !composed); if (compose_status == XKB_COMPOSE_CANCELLED) goto maybe_repeat; int count = composed ? xkb_compose_state_get_utf8(seat->kbd.xkb_compose_state, NULL, 0) : xkb_state_key_get_utf8(seat->kbd.xkb_state, key, NULL, 0); /* Buffer for translated key. Use a static buffer in most cases, * and use a malloc:ed buffer when necessary */ uint8_t buf[32]; uint8_t *utf8 = count < sizeof(buf) ? buf : xmalloc(count + 1); uint32_t *utf32 = NULL; if (composed) { xkb_compose_state_get_utf8( seat->kbd.xkb_compose_state, (char *)utf8, count + 1); if (count > 0) utf32 = ambstoc32((const char *)utf8); } else { xkb_state_key_get_utf8( seat->kbd.xkb_state, key, (char *)utf8, count + 1); utf32 = xcalloc(2, sizeof(utf32[0])); utf32[0] = xkb_state_key_get_utf32(seat->kbd.xkb_state, key); } struct kbd_ctx ctx = { .layout = layout_idx, .key = key, .sym = sym, .level0_syms = { .syms = raw_syms, .count = raw_count, }, .mods = mods, .consumed = consumed, .utf8 = { .buf = utf8, .count = count, }, .utf32 = utf32, .compose_status = compose_status, .key_state = state, }; bool handled = term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx] != 0 ? kitty_kbd_protocol(seat, term, &ctx) : legacy_kbd_protocol(seat, term, &ctx); if (composed && released) xkb_compose_state_reset(seat->kbd.xkb_compose_state); if (utf8 != buf) free(utf8); if (handled && !keysym_is_modifier(sym)) { term_reset_view(term); selection_cancel(term); } free(utf32); maybe_repeat: clock_gettime( term->wl->presentation_clock_id, &term->render.input_time); if (should_repeat) start_repeater(seat, key); } static void keyboard_key(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { struct seat *seat = data; key_press_release(seat, seat->kbd_focus, serial, key + 8, state); } static void keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, uint32_t mods_depressed, uint32_t mods_latched, uint32_t mods_locked, uint32_t group) { struct seat *seat = data; #if defined(_DEBUG) char depressed[256]; char latched[256]; char locked[256]; modifier_string(mods_depressed, sizeof(depressed), depressed, seat); modifier_string(mods_latched, sizeof(latched), latched, seat); modifier_string(mods_locked, sizeof(locked), locked, seat); LOG_DBG( "modifiers: depressed=%s (0x%x), latched=%s (0x%x), locked=%s (0x%x), " "group=%u", depressed, mods_depressed, latched, mods_latched, locked, mods_locked, group); #endif if (seat->kbd.xkb_state != NULL) { xkb_state_update_mask( seat->kbd.xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group); /* Update state of modifiers we're interested in for e.g mouse events */ seat->kbd.shift = seat->kbd.mod_shift != XKB_MOD_INVALID ? xkb_state_mod_index_is_active( seat->kbd.xkb_state, seat->kbd.mod_shift, XKB_STATE_MODS_EFFECTIVE) : false; seat->kbd.alt = seat->kbd.mod_alt != XKB_MOD_INVALID ? xkb_state_mod_index_is_active( seat->kbd.xkb_state, seat->kbd.mod_alt, XKB_STATE_MODS_EFFECTIVE) : false; seat->kbd.ctrl = seat->kbd.mod_ctrl != XKB_MOD_INVALID ? xkb_state_mod_index_is_active( seat->kbd.xkb_state, seat->kbd.mod_ctrl, XKB_STATE_MODS_EFFECTIVE) : false; seat->kbd.super = seat->kbd.mod_super != XKB_MOD_INVALID ? xkb_state_mod_index_is_active( seat->kbd.xkb_state, seat->kbd.mod_super, XKB_STATE_MODS_EFFECTIVE) : false; } if (seat->kbd_focus && seat->kbd_focus->active_surface == TERM_SURF_GRID) term_xcursor_update_for_seat(seat->kbd_focus, seat); } UNITTEST { int chan[2]; pipe2(chan, O_CLOEXEC); xassert(chan[0] >= 0); xassert(chan[1] >= 0); struct config conf = {0}; struct grid grid = {0}; struct terminal term = { .conf = &conf, .grid = &grid, .ptmx = chan[1], .selection = { .coords = { .start = {-1, -1}, .end = {-1, -1}, }, .auto_scroll = { .fd = -1, }, }, }; struct key_binding_manager *key_binding_manager = key_binding_manager_new(); struct wayland wayl = { .key_binding_manager = key_binding_manager, .terms = tll_init(), }; struct seat seat = { .wayl = &wayl, .name = "unittest", }; tll_push_back(wayl.terms, &term); term.wl = &wayl; seat.kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); xassert(seat.kbd.xkb != NULL); grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE; /* Swedish keymap */ { seat.kbd.xkb_keymap = xkb_keymap_new_from_names( seat.kbd.xkb, &(struct xkb_rule_names){.layout = "se"}, XKB_KEYMAP_COMPILE_NO_FLAGS); if (seat.kbd.xkb_keymap == NULL) { /* Skip test */ goto no_keymap; } seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); xassert(seat.kbd.xkb_state != NULL); seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); /* Significant modifiers in the legacy keyboard protocol */ seat.kbd.legacy_significant = 0; if (seat.kbd.mod_shift != XKB_MOD_INVALID) seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; if (seat.kbd.mod_alt != XKB_MOD_INVALID) seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; if (seat.kbd.mod_super != XKB_MOD_INVALID) seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; /* Significant modifiers in the kitty keyboard protocol */ seat.kbd.kitty_significant = seat.kbd.legacy_significant; if (seat.kbd.mod_caps != XKB_MOD_INVALID) seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; if (seat.kbd.mod_num != XKB_MOD_INVALID) seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; key_binding_new_for_seat(key_binding_manager, &seat); key_binding_load_keymap(key_binding_manager, &seat); { xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_ctrl; keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); key_press_release(&seat, &term, 1337, KEY_A + 8, WL_KEYBOARD_KEY_STATE_PRESSED); char escape[64] = {0}; ssize_t count = read(chan[0], escape, sizeof(escape)); /* key: 97 = 'a', alternate: 65 = 'A', base: N/A, mods: 6 = ctrl+shift */ const char expected_ctrl_shift_a[] = "\033[97:65;6u"; xassert(count == strlen(expected_ctrl_shift_a)); xassert(streq(escape, expected_ctrl_shift_a)); key_press_release(&seat, &term, 1337, KEY_A + 8, WL_KEYBOARD_KEY_STATE_RELEASED); } { xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt; keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_PRESSED); char escape[64] = {0}; ssize_t count = read(chan[0], escape, sizeof(escape)); /* key;. 50 = '2', alternate: 34 = '"', base: N/A, 4 = alt+shift */ const char expected_alt_shift_2[] = "\033[50:34;4u"; xassert(count == strlen(expected_alt_shift_2)); xassert(streq(escape, expected_alt_shift_2)); key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_RELEASED); } { xkb_mod_index_t alt_gr = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, "Mod5"); xassert(alt_gr != XKB_MOD_INVALID); xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt | 1u << alt_gr; keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_PRESSED); char escape[64] = {0}; ssize_t count = read(chan[0], escape, sizeof(escape)); /* key; 178 = '²', alternate: N/A, base: 50 = '2', 4 = alt+shift (AltGr not part of the protocol) */ const char expected_altgr_alt_shift_2[] = "\033[178::50;4u"; xassert(count == strlen(expected_altgr_alt_shift_2)); xassert(streq(escape, expected_altgr_alt_shift_2)); key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_RELEASED); } { xkb_mod_mask_t mods = 1u << seat.kbd.mod_alt; keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); char escape[64] = {0}; ssize_t count = read(chan[0], escape, sizeof(escape)); /* key; 127 = , alternate: N/A, base: N/A, 3 = alt */ const char expected_alt_backspace[] = "\033[127;3u"; xassert(count == strlen(expected_alt_backspace)); xassert(streq(escape, expected_alt_backspace)); key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); } { xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl; keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); key_press_release(&seat, &term, 1337, KEY_ENTER + 8, WL_KEYBOARD_KEY_STATE_PRESSED); char escape[64] = {0}; ssize_t count = read(chan[0], escape, sizeof(escape)); /* key; 13 = , alternate: N/A, base: N/A, 5 = ctrl */ const char expected_ctrl_enter[] = "\033[13;5u"; xassert(count == strlen(expected_ctrl_enter)); xassert(streq(escape, expected_ctrl_enter)); key_press_release(&seat, &term, 1337, KEY_ENTER + 8, WL_KEYBOARD_KEY_STATE_RELEASED); } { xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl; keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); key_press_release(&seat, &term, 1337, KEY_TAB + 8, WL_KEYBOARD_KEY_STATE_PRESSED); char escape[64] = {0}; ssize_t count = read(chan[0], escape, sizeof(escape)); /* key; 9 = , alternate: N/A, base: N/A, 5 = ctrl */ const char expected_ctrl_tab[] = "\033[9;5u"; xassert(count == strlen(expected_ctrl_tab)); xassert(streq(escape, expected_ctrl_tab)); key_press_release(&seat, &term, 1337, KEY_TAB + 8, WL_KEYBOARD_KEY_STATE_RELEASED); } { xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl | 1u << seat.kbd.mod_shift; keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); key_press_release(&seat, &term, 1337, KEY_LEFT + 8, WL_KEYBOARD_KEY_STATE_PRESSED); char escape[64] = {0}; ssize_t count = read(chan[0], escape, sizeof(escape)); const char expected_ctrl_shift_left[] = "\033[1;6D"; xassert(count == strlen(expected_ctrl_shift_left)); xassert(streq(escape, expected_ctrl_shift_left)); key_press_release(&seat, &term, 1337, KEY_LEFT + 8, WL_KEYBOARD_KEY_STATE_RELEASED); } key_binding_unload_keymap(key_binding_manager, &seat); key_binding_remove_seat(key_binding_manager, &seat); xkb_state_unref(seat.kbd.xkb_state); xkb_keymap_unref(seat.kbd.xkb_keymap); seat.kbd.xkb_state = NULL; seat.kbd.xkb_keymap = NULL; } /* de(neo) keymap */ { seat.kbd.xkb_keymap = xkb_keymap_new_from_names( seat.kbd.xkb, &(struct xkb_rule_names){.layout = "us,de(neo)"}, XKB_KEYMAP_COMPILE_NO_FLAGS); if (seat.kbd.xkb_keymap == NULL) { /* Skip test */ goto no_keymap; } seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); xassert(seat.kbd.xkb_state != NULL); seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); /* Significant modifiers in the legacy keyboard protocol */ seat.kbd.legacy_significant = 0; if (seat.kbd.mod_shift != XKB_MOD_INVALID) seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; if (seat.kbd.mod_alt != XKB_MOD_INVALID) seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; if (seat.kbd.mod_super != XKB_MOD_INVALID) seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; /* Significant modifiers in the kitty keyboard protocol */ seat.kbd.kitty_significant = seat.kbd.legacy_significant; if (seat.kbd.mod_caps != XKB_MOD_INVALID) seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; if (seat.kbd.mod_num != XKB_MOD_INVALID) seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; key_binding_new_for_seat(key_binding_manager, &seat); key_binding_load_keymap(key_binding_manager, &seat); { /* * In the de(neo) layout, the Y key generates 'k'. This * means we should get a key+alternate that indicates 'k', * but a base key that is 'y'. */ xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt; keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1); key_press_release(&seat, &term, 1337, KEY_Y + 8, WL_KEYBOARD_KEY_STATE_PRESSED); char escape[64] = {0}; ssize_t count = read(chan[0], escape, sizeof(escape)); /* key: 107 = 'k', alternate: 75 = 'K', base: 121 = 'y', mods: 4 = alt+shift */ const char expected_alt_shift_y[] = "\033[107:75:121;4u"; xassert(count == strlen(expected_alt_shift_y)); xassert(streq(escape, expected_alt_shift_y)); key_press_release(&seat, &term, 1337, KEY_Y + 8, WL_KEYBOARD_KEY_STATE_RELEASED); } key_binding_unload_keymap(key_binding_manager, &seat); key_binding_remove_seat(key_binding_manager, &seat); xkb_state_unref(seat.kbd.xkb_state); xkb_keymap_unref(seat.kbd.xkb_keymap); seat.kbd.xkb_state = NULL; seat.kbd.xkb_keymap = NULL; } /* us(intl) keymap */ { seat.kbd.xkb_keymap = xkb_keymap_new_from_names( seat.kbd.xkb, &(struct xkb_rule_names){.layout = "us", .variant = "intl"}, XKB_KEYMAP_COMPILE_NO_FLAGS); if (seat.kbd.xkb_keymap == NULL) { /* Skip test */ goto no_keymap; } seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); xassert(seat.kbd.xkb_state != NULL); seat.kbd.xkb_compose_table = xkb_compose_table_new_from_locale( seat.kbd.xkb, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); if (seat.kbd.xkb_compose_table == NULL) goto no_keymap; seat.kbd.xkb_compose_state = xkb_compose_state_new( seat.kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); if (seat.kbd.xkb_compose_state == NULL) { xkb_compose_table_unref(seat.kbd.xkb_compose_table); goto no_keymap; } seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); /* Significant modifiers in the legacy keyboard protocol */ seat.kbd.legacy_significant = 0; if (seat.kbd.mod_shift != XKB_MOD_INVALID) seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; if (seat.kbd.mod_alt != XKB_MOD_INVALID) seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; if (seat.kbd.mod_super != XKB_MOD_INVALID) seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; /* Significant modifiers in the kitty keyboard protocol */ seat.kbd.kitty_significant = seat.kbd.legacy_significant; if (seat.kbd.mod_caps != XKB_MOD_INVALID) seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; if (seat.kbd.mod_num != XKB_MOD_INVALID) seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; key_binding_new_for_seat(key_binding_manager, &seat); key_binding_load_keymap(key_binding_manager, &seat); { /* * Test the compose sequence "shift+', shift+space" * * Should result in a double quote, but a regression * caused it to instead emit a space. See #1987 * * Note: "shift+', space" also results in a double quote, * but never regressed to a space. */ grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE; xkb_compose_state_reset(seat.kbd.xkb_compose_state); xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift; keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1); key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); key_press_release(&seat, &term, 1337, KEY_SPACE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); char escape[64] = {0}; ssize_t count = read(chan[0], escape, sizeof(escape)); /* key: 34 = '"', alternate: N/A, base: N/A, mods: 2 = shift */ const char expected_shift_apostrophe[] = "\033[34;2u"; xassert(count == strlen(expected_shift_apostrophe)); xassert(streq(escape, expected_shift_apostrophe)); key_press_release(&seat, &term, 1337, KEY_SPACE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE; } key_binding_unload_keymap(key_binding_manager, &seat); key_binding_remove_seat(key_binding_manager, &seat); xkb_compose_state_unref(seat.kbd.xkb_compose_state); xkb_compose_table_unref(seat.kbd.xkb_compose_table); xkb_state_unref(seat.kbd.xkb_state); xkb_keymap_unref(seat.kbd.xkb_keymap); seat.kbd.xkb_state = NULL; seat.kbd.xkb_keymap = NULL; } no_keymap: xkb_context_unref(seat.kbd.xkb); key_binding_manager_destroy(key_binding_manager); tll_free(wayl.terms); close(chan[0]); close(chan[1]); } static void keyboard_repeat_info(void *data, struct wl_keyboard *wl_keyboard, int32_t rate, int32_t delay) { struct seat *seat = data; LOG_DBG("keyboard repeat: rate=%d, delay=%d", rate, delay); seat->kbd.repeat.rate = rate; seat->kbd.repeat.delay = delay; } const struct wl_keyboard_listener keyboard_listener = { .keymap = &keyboard_keymap, .enter = &keyboard_enter, .leave = &keyboard_leave, .key = &keyboard_key, .modifiers = &keyboard_modifiers, .repeat_info = &keyboard_repeat_info, }; void input_repeat(struct seat *seat, uint32_t key) { /* Should be cleared as soon as we loose focus */ xassert(seat->kbd_focus != NULL); struct terminal *term = seat->kbd_focus; key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_DOWN); } static bool is_top_left(const struct terminal *term, int x, int y) { int csd_border_size = term->conf->csd.border_width; return ( (!term->window->is_tiled_top && !term->window->is_tiled_left) && ((term->active_surface == TERM_SURF_BORDER_LEFT && y < 10 * term->scale) || (term->active_surface == TERM_SURF_BORDER_TOP && x < (10 + csd_border_size) * term->scale))); } static bool is_top_right(const struct terminal *term, int x, int y) { int csd_border_size = term->conf->csd.border_width; return ( (!term->window->is_tiled_top && !term->window->is_tiled_right) && ((term->active_surface == TERM_SURF_BORDER_RIGHT && y < 10 * term->scale) || (term->active_surface == TERM_SURF_BORDER_TOP && x > term->width + 1 * csd_border_size * term->scale - 10 * term->scale))); } static bool is_bottom_left(const struct terminal *term, int x, int y) { int csd_title_size = term->conf->csd.title_height; int csd_border_size = term->conf->csd.border_width; return ( (!term->window->is_tiled_bottom && !term->window->is_tiled_left) && ((term->active_surface == TERM_SURF_BORDER_LEFT && y > csd_title_size * term->scale + term->height) || (term->active_surface == TERM_SURF_BORDER_BOTTOM && x < (10 + csd_border_size) * term->scale))); } static bool is_bottom_right(const struct terminal *term, int x, int y) { int csd_title_size = term->conf->csd.title_height; int csd_border_size = term->conf->csd.border_width; return ( (!term->window->is_tiled_bottom && !term->window->is_tiled_right) && ((term->active_surface == TERM_SURF_BORDER_RIGHT && y > csd_title_size * term->scale + term->height) || (term->active_surface == TERM_SURF_BORDER_BOTTOM && x > term->width + 1 * csd_border_size * term->scale - 10 * term->scale))); } enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y) { if (is_top_left(term, x, y)) return CURSOR_SHAPE_TOP_LEFT_CORNER; else if (is_top_right(term, x, y)) return CURSOR_SHAPE_TOP_RIGHT_CORNER; else if (is_bottom_left(term, x, y)) return CURSOR_SHAPE_BOTTOM_LEFT_CORNER; else if (is_bottom_right(term, x, y)) return CURSOR_SHAPE_BOTTOM_RIGHT_CORNER; else if (term->active_surface == TERM_SURF_BORDER_LEFT) return CURSOR_SHAPE_LEFT_SIDE; else if (term->active_surface == TERM_SURF_BORDER_RIGHT) return CURSOR_SHAPE_RIGHT_SIDE; else if (term->active_surface == TERM_SURF_BORDER_TOP) return CURSOR_SHAPE_TOP_SIDE; else if (term->active_surface == TERM_SURF_BORDER_BOTTOM) return CURSOR_SHAPE_BOTTOM_SIDE; else { BUG("Unreachable"); return CURSOR_SHAPE_NONE; } } static void mouse_button_state_reset(struct seat *seat) { tll_free(seat->mouse.buttons); seat->mouse.count = 0; seat->mouse.last_released_button = 0; memset(&seat->mouse.last_time, 0, sizeof(seat->mouse.last_time)); } static void mouse_coord_pixel_to_cell(struct seat *seat, const struct terminal *term, int x, int y) { /* * Translate x,y pixel coordinate to a cell coordinate, or -1 * if the cursor is outside the grid. I.e. if it is inside the * margins. */ if (x < term->margins.left) seat->mouse.col = 0; else if (x >= term->width - term->margins.right) seat->mouse.col = term->cols - 1; else seat->mouse.col = (x - term->margins.left) / term->cell_width; if (y < term->margins.top) seat->mouse.row = 0; else if (y >= term->height - term->margins.bottom) seat->mouse.row = term->rows - 1; else seat->mouse.row = (y - term->margins.top) / term->cell_height; } static bool touch_is_active(const struct seat *seat) { if (seat->wl_touch == NULL) { return false; } switch (seat->touch.state) { case TOUCH_STATE_IDLE: case TOUCH_STATE_INHIBITED: return false; case TOUCH_STATE_HELD: case TOUCH_STATE_DRAGGING: case TOUCH_STATE_SCROLLING: return true; } BUG("Bad touch state: %d", seat->touch.state); return false; } static void wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { if (unlikely(surface == NULL)) { /* Seen on mutter-3.38 */ LOG_WARN("compositor sent pointer_enter event with a NULL surface"); return; } struct seat *seat = data; struct wl_window *win = wl_surface_get_user_data(surface); struct terminal *term = win->term; seat->mouse_focus = term; term->active_surface = term_surface_kind(term, surface); if (touch_is_active(seat)) return; int x = wl_fixed_to_int(surface_x) * term->scale; int y = wl_fixed_to_int(surface_y) * term->scale; seat->pointer.serial = serial; seat->pointer.hidden = false; seat->mouse.x = x; seat->mouse.y = y; LOG_DBG("pointer-enter: pointer=%p, serial=%u, surface = %p, new-moused = %p, " "x=%d, y=%d", (void *)wl_pointer, serial, (void *)surface, (void *)term, x, y); xassert(tll_length(seat->mouse.buttons) == 0); wayl_reload_xcursor_theme(seat, term->scale); /* Scale may have changed */ term_xcursor_update_for_seat(term, seat); switch (term->active_surface) { case TERM_SURF_GRID: { mouse_coord_pixel_to_cell(seat, term, x, y); break; } case TERM_SURF_TITLE: case TERM_SURF_BORDER_LEFT: case TERM_SURF_BORDER_RIGHT: case TERM_SURF_BORDER_TOP: case TERM_SURF_BORDER_BOTTOM: break; case TERM_SURF_BUTTON_MINIMIZE: case TERM_SURF_BUTTON_MAXIMIZE: case TERM_SURF_BUTTON_CLOSE: render_refresh_csd(term); break; case TERM_SURF_NONE: BUG("Invalid surface type"); break; } } static void wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *surface) { struct seat *seat = data; if (seat->wl_touch != NULL) { switch (seat->touch.state) { case TOUCH_STATE_IDLE: break; case TOUCH_STATE_INHIBITED: seat->touch.state = TOUCH_STATE_IDLE; break; case TOUCH_STATE_HELD: case TOUCH_STATE_DRAGGING: case TOUCH_STATE_SCROLLING: return; } } struct terminal *old_moused = seat->mouse_focus; LOG_DBG( "%s: pointer-leave: pointer=%p, serial=%u, surface = %p, old-moused = %p", seat->name, (void *)wl_pointer, serial, (void *)surface, (void *)old_moused); seat->pointer.hidden = false; if (seat->pointer.xcursor_callback != NULL) { /* A cursor frame callback may never be called if the pointer leaves our surface */ wl_callback_destroy(seat->pointer.xcursor_callback); seat->pointer.xcursor_callback = NULL; seat->pointer.xcursor_pending = false; } /* Reset last-set-xcursor, to ensure we update it on a pointer-enter event */ seat->pointer.shape = CURSOR_SHAPE_NONE; /* Reset mouse state */ seat->mouse.x = seat->mouse.y = 0; seat->mouse.col = seat->mouse.row = 0; mouse_button_state_reset(seat); for (size_t i = 0; i < ALEN(seat->mouse.aggregated); i++) seat->mouse.aggregated[i] = 0.0; seat->mouse.have_discrete = false; seat->mouse_focus = NULL; if (old_moused == NULL) { LOG_WARN( "compositor sent pointer_leave event without a pointer_enter " "event: surface=%p", (void *)surface); } else { if (surface != NULL) { /* Sway 1.4 sends this event with a NULL surface when we destroy the window */ const struct wl_window UNUSED *win = wl_surface_get_user_data(surface); xassert(old_moused == win->term); } enum term_surface active_surface = old_moused->active_surface; old_moused->active_surface = TERM_SURF_NONE; switch (active_surface) { case TERM_SURF_BUTTON_MINIMIZE: case TERM_SURF_BUTTON_MAXIMIZE: case TERM_SURF_BUTTON_CLOSE: if (old_moused->shutdown.in_progress) break; render_refresh_csd(old_moused); break; case TERM_SURF_GRID: selection_finalize(seat, old_moused, seat->pointer.serial); break; case TERM_SURF_NONE: case TERM_SURF_TITLE: case TERM_SURF_BORDER_LEFT: case TERM_SURF_BORDER_RIGHT: case TERM_SURF_BORDER_TOP: case TERM_SURF_BORDER_BOTTOM: break; } } } static bool pointer_is_on_button(const struct terminal *term, const struct seat *seat, enum csd_surface csd_surface) { if (seat->mouse.x < 0) return false; if (seat->mouse.y < 0) return false; struct csd_data info = get_csd_data(term, csd_surface); if (seat->mouse.x > info.width) return false; if (seat->mouse.y > info.height) return false; return true; } static void wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) { struct seat *seat = data; /* Touch-emulated pointer events have wl_pointer == NULL. */ if (wl_pointer != NULL && touch_is_active(seat)) return; struct wayland *wayl = seat->wayl; struct terminal *term = seat->mouse_focus; if (unlikely(term == NULL)) { /* Typically happens when the compositor sent a pointer enter * event with a NULL surface - see wl_pointer_enter(). * * In this case, we never set seat->mouse_focus (since we * can't map the enter event to a specific window). */ return; } struct wl_window *win = term->window; LOG_DBG("pointer_motion: pointer=%p, x=%d, y=%d", (void *)wl_pointer, wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); xassert(term != NULL); int x = wl_fixed_to_int(surface_x) * term->scale; int y = wl_fixed_to_int(surface_y) * term->scale; enum term_surface surf_kind = term->active_surface; int button = 0; bool send_to_client = false; bool is_on_button = false; /* If current surface is a button, check if pointer was on it *before* the motion event */ switch (surf_kind) { case TERM_SURF_BUTTON_MINIMIZE: is_on_button = pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE); break; case TERM_SURF_BUTTON_MAXIMIZE: is_on_button = pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE); break; case TERM_SURF_BUTTON_CLOSE: is_on_button = pointer_is_on_button(term, seat, CSD_SURF_CLOSE); break; case TERM_SURF_NONE: case TERM_SURF_GRID: case TERM_SURF_TITLE: case TERM_SURF_BORDER_LEFT: case TERM_SURF_BORDER_RIGHT: case TERM_SURF_BORDER_TOP: case TERM_SURF_BORDER_BOTTOM: break; } seat->pointer.hidden = false; seat->mouse.x = x; seat->mouse.y = y; term_xcursor_update_for_seat(term, seat); if (tll_length(seat->mouse.buttons) > 0) { const struct button_tracker *tracker = &tll_front(seat->mouse.buttons); surf_kind = tracker->surf_kind; button = tracker->button; send_to_client = tracker->send_to_client; } switch (surf_kind) { case TERM_SURF_NONE: break; case TERM_SURF_BUTTON_MINIMIZE: if (pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE) != is_on_button) render_refresh_csd(term); break; case TERM_SURF_BUTTON_MAXIMIZE: if (pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE) != is_on_button) render_refresh_csd(term); break; case TERM_SURF_BUTTON_CLOSE: if (pointer_is_on_button(term, seat, CSD_SURF_CLOSE) != is_on_button) render_refresh_csd(term); break; case TERM_SURF_TITLE: /* We've started a 'move' timer, but user started dragging * right away - abort the timer and initiate the actual move * right away */ if (button == BTN_LEFT && win->csd.move_timeout_fd != -1) { fdm_del(wayl->fdm, win->csd.move_timeout_fd); win->csd.move_timeout_fd = -1; xdg_toplevel_move(win->xdg_toplevel, seat->wl_seat, win->csd.serial); } break; case TERM_SURF_BORDER_LEFT: case TERM_SURF_BORDER_RIGHT: case TERM_SURF_BORDER_TOP: case TERM_SURF_BORDER_BOTTOM: break; case TERM_SURF_GRID: { int old_col = seat->mouse.col; int old_row = seat->mouse.row; mouse_coord_pixel_to_cell(seat, term, seat->mouse.x, seat->mouse.y); xassert(seat->mouse.col >= 0 && seat->mouse.col < term->cols); xassert(seat->mouse.row >= 0 && seat->mouse.row < term->rows); /* Cursor has moved to a different cell since last time */ bool cursor_is_on_new_cell = old_col != seat->mouse.col || old_row != seat->mouse.row; if (cursor_is_on_new_cell) { /* Prevent multiple/different mouse bindings from * triggering if the mouse has moved "too much" (to * another cell) */ seat->mouse.count = 0; } /* Cursor is inside the grid, i.e. *not* in the margins */ const bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0; enum selection_scroll_direction auto_scroll_direction = term->selection.coords.end.row < 0 ? SELECTION_SCROLL_NOT : y < term->margins.top ? SELECTION_SCROLL_UP : y > term->height - term->margins.bottom ? SELECTION_SCROLL_DOWN : SELECTION_SCROLL_NOT; if (auto_scroll_direction == SELECTION_SCROLL_NOT) selection_stop_scroll_timer(term); /* Update selection */ if (!term->is_searching) { if (auto_scroll_direction != SELECTION_SCROLL_NOT) { /* * Start 'selection auto-scrolling' * * The speed of the scrolling is proportional to the * distance between the mouse and the grid; the * further away the mouse is, the faster we scroll. * * Note that the speed is measured in 'intervals (in * ns) between each timed scroll of a single line'. * * Thus, the further away the mouse is, the smaller * interval value we use. */ int distance = auto_scroll_direction == SELECTION_SCROLL_UP ? term->margins.top - y : y - (term->height - term->margins.bottom); xassert(distance > 0); int divisor = distance * term->conf->scrollback.multiplier / term->scale; selection_start_scroll_timer( term, 400000000 / (divisor > 0 ? divisor : 1), auto_scroll_direction, seat->mouse.col); } if (term->selection.ongoing && (cursor_is_on_new_cell || (term->selection.coords.end.row < 0 && seat->mouse.x >= term->margins.left && seat->mouse.x < term->width - term->margins.right && seat->mouse.y >= term->margins.top && seat->mouse.y < term->height - term->margins.bottom))) { selection_update(term, seat->mouse.col, seat->mouse.row); } } /* Send mouse event to client application */ if (!term_mouse_grabbed(term, seat) && (cursor_is_on_new_cell || term->mouse_reporting == MOUSE_SGR_PIXELS) && ((button == 0 && cursor_is_on_grid) || (button != 0 && send_to_client))) { xassert(seat->mouse.col < term->cols); xassert(seat->mouse.row < term->rows); term_mouse_motion( term, button, seat->mouse.row, seat->mouse.col, seat->mouse.y - term->margins.top, seat->mouse.x - term->margins.left, seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); } break; } } } static bool fdm_csd_move(struct fdm *fdm, int fd, int events, void *data) { struct seat *seat = data; fdm_del(fdm, fd); if (seat->mouse_focus == NULL) { LOG_WARN( "%s: CSD move timeout triggered, but seat's has no mouse focused terminal", seat->name); return true; } struct wl_window *win = seat->mouse_focus->window; win->csd.move_timeout_fd = -1; xdg_toplevel_move(win->xdg_toplevel, seat->wl_seat, win->csd.serial); return true; } static const struct key_binding * match_mouse_binding(const struct seat *seat, const struct terminal *term, int button) { if (seat->wl_keyboard != NULL && seat->kbd.xkb_state != NULL) { /* Seat has keyboard - use mouse bindings *with* modifiers */ const struct key_binding_set *bindings = key_binding_for(term->wl->key_binding_manager, term->conf, seat); xassert(bindings != NULL); xkb_mod_mask_t mods; get_current_modifiers(seat, &mods, NULL, 0, true); /* Ignore selection override modifiers when * matching modifiers */ mods &= ~bindings->selection_overrides; const struct key_binding *match = NULL; tll_foreach(bindings->mouse, it) { const struct key_binding *binding = &it->item; if (binding->m.button != button) { /* Wrong button */ continue; } if (binding->mods != mods) { /* Modifier mismatch */ continue; } if (binding->m.count > seat->mouse.count) { /* Not correct click count */ continue; } if (match == NULL || binding->m.count > match->m.count) match = binding; } return match; } else { /* Seat does NOT have a keyboard - use mouse bindings *without* * modifiers */ const struct config_key_binding *match = NULL; const struct config *conf = term->conf; for (size_t i = 0; i < conf->bindings.mouse.count; i++) { const struct config_key_binding *binding = &conf->bindings.mouse.arr[i]; if (binding->m.button != button) { /* Wrong button */ continue; } if (binding->m.count > seat->mouse.count) { /* Incorrect click count */ continue; } if (tll_length(binding->modifiers) > 0) { /* Binding has modifiers */ continue; } if (match == NULL || binding->m.count > match->m.count) match = binding; } if (match != NULL) { static struct key_binding bind; bind.action = match->action; bind.aux = &match->aux; return &bind; } return NULL; } BUG("should not get here"); } static void wl_pointer_button(void *data, struct wl_pointer *wl_pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state) { LOG_DBG("BUTTON: pointer=%p, serial=%u, button=%x, state=%u", (void *)wl_pointer, serial, button, state); xassert(serial != 0); struct seat *seat = data; /* Touch-emulated pointer events have wl_pointer == NULL. */ if (wl_pointer != NULL && touch_is_active(seat)) return; struct wayland *wayl = seat->wayl; struct terminal *term = seat->mouse_focus; seat->pointer.serial = serial; seat->pointer.hidden = false; xassert(term != NULL); enum term_surface surf_kind = TERM_SURF_NONE; bool send_to_client = false; if (state == WL_POINTER_BUTTON_STATE_PRESSED) { if (seat->wl_touch != NULL && seat->touch.state == TOUCH_STATE_IDLE) { seat->touch.state = TOUCH_STATE_INHIBITED; } /* Time since last click */ struct timespec now, since_last; clock_gettime(CLOCK_MONOTONIC, &now); timespec_sub(&now, &seat->mouse.last_time, &since_last); if (seat->mouse.last_released_button == button && since_last.tv_sec == 0 && since_last.tv_nsec <= 300 * 1000 * 1000) { seat->mouse.count++; } else seat->mouse.count = 1; /* * Workaround GNOME bug * * Dragging the window, then stopping the drag (releasing the * mouse button), *without* moving the mouse, and then * clicking twice, waiting for the CSD timer, and finally * clicking once more, results in the following sequence * (keyboard and other irrelevant events filtered out, unless * they're needed to prove a point): * * dbg: input.c:1551: cancelling drag timer, moving window * dbg: input.c:759: keyboard_leave: keyboard=0x607000003580, serial=873, surface=0x6070000036d0 * dbg: input.c:1432: seat0: pointer-leave: pointer=0x607000003660, serial=874, surface = 0x6070000396e0, old-moused = 0x622000006100 * * --> drag stopped here * * --> LMB clicked first time after the drag (generates the * enter event on *release*, but no button events) * dbg: input.c:1360: pointer-enter: pointer=0x607000003660, serial=876, surface = 0x6070000396e0, new-moused = 0x622000006100 * * --> LMB clicked, and held until the timer times out, second * time after the drag * dbg: input.c:1712: BUTTON: pointer=0x607000003660, serial=877, button=110, state=1 * dbg: input.c:1806: starting move timer * dbg: input.c:1692: move timer timed out * dbg: input.c:759: keyboard_leave: keyboard=0x607000003580, serial=878, surface=0x6070000036d0 * * --> NOTE: ^^ no pointer leave event this time, only the * keyboard leave * * --> LMB clicked one last time * dbg: input.c:697: seat0: keyboard_enter: keyboard=0x607000003580, serial=879, surface=0x6070000036d0 * dbg: input.c:1712: BUTTON: pointer=0x607000003660, serial=880, button=110, state=1 * err: input.c:1741: BUG in wl_pointer_button(): assertion failed: 'it->item.button != button' * * What are we seeing? * * - GNOME does *not* send a pointer *enter* event after the drag * has stopped * - The second drag does *not* generate a pointer *leave* event * - The missing leave event means we're still tracking LMB as * being held down in our seat struct. * - This leads to an assert (debug builds) when LMB is clicked * again (seat's button list already contains LMB). * * Note: I've also observed variants of the above */ tll_foreach(seat->mouse.buttons, it) { if (it->item.button == button) { LOG_WARN("multiple button press events for button %d " "(compositor bug?)", button); tll_remove(seat->mouse.buttons, it); break; } } #if defined(_DEBUG) tll_foreach(seat->mouse.buttons, it) xassert(it->item.button != button); #endif /* * Remember which surface "owns" this button, so that we can * send motion and button release events to that surface, even * if the pointer is no longer over it. */ tll_push_back( seat->mouse.buttons, ((struct button_tracker){ .button = button, .surf_kind = term->active_surface, .send_to_client = false})); seat->mouse.last_time = now; surf_kind = term->active_surface; send_to_client = false; /* For now, may be set to true if a binding consumes the button */ } else { bool UNUSED have_button = false; tll_foreach(seat->mouse.buttons, it) { if (it->item.button == button) { have_button = true; surf_kind = it->item.surf_kind; send_to_client = it->item.send_to_client; tll_remove(seat->mouse.buttons, it); break; } } if (seat->wl_touch != NULL && seat->touch.state == TOUCH_STATE_INHIBITED) { if (tll_length(seat->mouse.buttons) == 0) { seat->touch.state = TOUCH_STATE_IDLE; } } if (!have_button) { /* * Seen on Sway with slurp * * 1. Run slurp * 2. Press, and hold left mouse button * 3. Press escape, to cancel slurp * 4. Release mouse button * 5. BAM! */ LOG_WARN("stray button release event (compositor bug?)"); return; } seat->mouse.last_released_button = button; } switch (surf_kind) { case TERM_SURF_TITLE: if (state == WL_POINTER_BUTTON_STATE_PRESSED) { struct wl_window *win = term->window; /* Toggle maximized state on double-click */ if (term->conf->csd.double_click_to_maximize && button == BTN_LEFT && seat->mouse.count == 2) { if (win->is_maximized) xdg_toplevel_unset_maximized(win->xdg_toplevel); else xdg_toplevel_set_maximized(win->xdg_toplevel); } else if (button == BTN_LEFT && win->csd.move_timeout_fd < 0) { const struct itimerspec timeout = { .it_value = {.tv_nsec = 200000000}, }; int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (fd >= 0 && timerfd_settime(fd, 0, &timeout, NULL) == 0 && fdm_add(wayl->fdm, fd, EPOLLIN, &fdm_csd_move, seat)) { win->csd.move_timeout_fd = fd; win->csd.serial = serial; } else { LOG_ERRNO("failed to configure XDG toplevel move timer FD"); if (fd >= 0) close(fd); } } if (button == BTN_RIGHT && tll_length(seat->mouse.buttons) == 1) { const struct csd_data info = get_csd_data(term, CSD_SURF_TITLE); xdg_toplevel_show_window_menu( win->xdg_toplevel, seat->wl_seat, seat->pointer.serial, seat->mouse.x + info.x, seat->mouse.y + info.y); } } else if (state == WL_POINTER_BUTTON_STATE_RELEASED) { struct wl_window *win = term->window; if (win->csd.move_timeout_fd >= 0) { fdm_del(wayl->fdm, win->csd.move_timeout_fd); win->csd.move_timeout_fd = -1; } } return; case TERM_SURF_BORDER_LEFT: case TERM_SURF_BORDER_RIGHT: case TERM_SURF_BORDER_TOP: case TERM_SURF_BORDER_BOTTOM: { 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; int x = seat->mouse.x; int y = seat->mouse.y; if (is_top_left(term, x, y)) resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT; else if (is_top_right(term, x, y)) resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT; else if (is_bottom_left(term, x, y)) resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT; else if (is_bottom_right(term, x, y)) resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT; else resize_type = map[term->active_surface]; xdg_toplevel_resize( term->window->xdg_toplevel, seat->wl_seat, serial, resize_type); } return; } case TERM_SURF_BUTTON_MINIMIZE: if (button == BTN_LEFT && pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE) && state == WL_POINTER_BUTTON_STATE_RELEASED) { xdg_toplevel_set_minimized(term->window->xdg_toplevel); } break; case TERM_SURF_BUTTON_MAXIMIZE: if (button == BTN_LEFT && pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE) && state == WL_POINTER_BUTTON_STATE_RELEASED) { if (term->window->is_maximized) xdg_toplevel_unset_maximized(term->window->xdg_toplevel); else xdg_toplevel_set_maximized(term->window->xdg_toplevel); } break; case TERM_SURF_BUTTON_CLOSE: if (button == BTN_LEFT && pointer_is_on_button(term, seat, CSD_SURF_CLOSE) && state == WL_POINTER_BUTTON_STATE_RELEASED) { term_shutdown(term); } break; case TERM_SURF_GRID: { search_cancel(term); urls_reset(term); bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0; switch (state) { case WL_POINTER_BUTTON_STATE_PRESSED: { bool consumed = false; if (cursor_is_on_grid && term_mouse_grabbed(term, seat)) { const struct key_binding *match = match_mouse_binding(seat, term, button); if (match != NULL) consumed = execute_binding(seat, term, match, serial, 1); } send_to_client = !consumed && cursor_is_on_grid; if (send_to_client) tll_back(seat->mouse.buttons).send_to_client = true; if (send_to_client && !term_mouse_grabbed(term, seat) && cursor_is_on_grid) { term_mouse_down( term, button, seat->mouse.row, seat->mouse.col, seat->mouse.y - term->margins.top, seat->mouse.x - term->margins.left, seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); } break; } case WL_POINTER_BUTTON_STATE_RELEASED: selection_finalize(seat, term, serial); if (send_to_client && !term_mouse_grabbed(term, seat)) { term_mouse_up( term, button, seat->mouse.row, seat->mouse.col, seat->mouse.y - term->margins.top, seat->mouse.x - term->margins.left, seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); } break; } break; } case TERM_SURF_NONE: BUG("Invalid surface type"); break; } } static void alternate_scroll(struct seat *seat, int amount, int button) { if (seat->wl_keyboard == NULL) return; /* Should be cleared in leave event */ xassert(seat->mouse_focus != NULL); struct terminal *term = seat->mouse_focus; assert(button == BTN_BACK || button == BTN_FORWARD); xkb_keycode_t key = button == BTN_BACK ? seat->kbd.key_arrow_up : seat->kbd.key_arrow_down; for (int i = 0; i < amount; i++) key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_DOWN); key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_UP); } static void mouse_scroll(struct seat *seat, int amount, enum wl_pointer_axis axis) { struct terminal *term = seat->mouse_focus; xassert(term != NULL); int button = axis == WL_POINTER_AXIS_VERTICAL_SCROLL ? amount < 0 ? BTN_WHEEL_BACK : BTN_WHEEL_FORWARD : amount < 0 ? BTN_WHEEL_LEFT : BTN_WHEEL_RIGHT; amount = abs(amount); if (term_mouse_grabbed(term, seat)) { seat->mouse.count = 1; const struct key_binding *match = match_mouse_binding(seat, term, button); if (match != NULL) execute_binding(seat, term, match, seat->pointer.serial, amount); seat->mouse.last_released_button = button; } else if (seat->mouse.col >= 0 && seat->mouse.row >= 0) { xassert(seat->mouse.col < term->cols); xassert(seat->mouse.row < term->rows); for (int i = 0; i < amount; i++) { term_mouse_down( term, button, seat->mouse.row, seat->mouse.col, seat->mouse.y - term->margins.top, seat->mouse.x - term->margins.left, seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); } term_mouse_up( term, button, seat->mouse.row, seat->mouse.col, seat->mouse.y - term->margins.top, seat->mouse.x - term->margins.left, seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); } } static double mouse_scroll_multiplier(const struct terminal *term, const struct seat *seat) { return (term->grid == &term->normal || (term_mouse_grabbed(term, seat) && term->alt_scrolling)) ? term->conf->scrollback.multiplier : 1.0; } static void wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis, wl_fixed_t value) { struct seat *seat = data; if (touch_is_active(seat)) return; if (seat->mouse.have_discrete) return; xassert(seat->mouse_focus != NULL); xassert(axis < ALEN(seat->mouse.aggregated)); const struct terminal *term = seat->mouse_focus; /* * Aggregate scrolled amount until we get at least 1.0 * * Without this, very slow scrolling will never actually scroll * anything. */ seat->mouse.aggregated[axis] += mouse_scroll_multiplier(term, seat) * wl_fixed_to_double(value); if (fabs(seat->mouse.aggregated[axis]) < seat->mouse_focus->cell_height) return; int lines = seat->mouse.aggregated[axis] / seat->mouse_focus->cell_height; mouse_scroll(seat, lines, axis); seat->mouse.aggregated[axis] -= (double)lines * seat->mouse_focus->cell_height; } static void wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, enum wl_pointer_axis axis, int32_t discrete) { LOG_DBG("axis_discrete: %d", discrete); struct seat *seat = data; if (touch_is_active(seat)) return; seat->mouse.have_discrete = true; int amount = discrete; if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) { /* Treat mouse wheel left/right as regular buttons */ } else amount *= mouse_scroll_multiplier(seat->mouse_focus, seat); mouse_scroll(seat, amount, axis); } #if defined(WL_POINTER_AXIS_VALUE120_SINCE_VERSION) static void wl_pointer_axis_value120(void *data, struct wl_pointer *wl_pointer, enum wl_pointer_axis axis, int32_t value120) { LOG_DBG("axis_value120: %d -> %.2f", value120, (float)value120 / 120.); struct seat *seat = data; if (touch_is_active(seat)) return; seat->mouse.have_discrete = true; /* * 120 corresponds to a single "low-res" scroll step. * * When doing high-res scrolling, take the scrollback.multiplier, * and calculate how many degrees there are per line. * * For example, with scrollback.multiplier = 3, we have 120 / 3 == 40. * * Then, accumulate high-res scroll events, until we have *at * least* that much. Translate the accumulated value to number of * lines, and scroll. * * Subtract the "used" degrees from the accumulated value, and * keep what's left (this value will always be less than the * per-line value). */ const double multiplier = mouse_scroll_multiplier(seat->mouse_focus, seat); const double per_line = 120. / multiplier; seat->mouse.aggregated_120[axis] += (double)value120; if (fabs(seat->mouse.aggregated_120[axis]) < per_line) return; int lines = (int)(seat->mouse.aggregated_120[axis] / per_line); mouse_scroll(seat, lines, axis); seat->mouse.aggregated_120[axis] -= (double)lines * per_line; } #endif static void wl_pointer_frame(void *data, struct wl_pointer *wl_pointer) { struct seat *seat = data; if (touch_is_active(seat)) return; seat->mouse.have_discrete = false; } static void wl_pointer_axis_source(void *data, struct wl_pointer *wl_pointer, uint32_t axis_source) { } static void wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis) { struct seat *seat = data; if (touch_is_active(seat)) return; xassert(axis < ALEN(seat->mouse.aggregated)); seat->mouse.aggregated[axis] = 0.; } const struct wl_pointer_listener pointer_listener = { .enter = &wl_pointer_enter, .leave = &wl_pointer_leave, .motion = &wl_pointer_motion, .button = &wl_pointer_button, .axis = &wl_pointer_axis, .frame = &wl_pointer_frame, .axis_source = &wl_pointer_axis_source, .axis_stop = &wl_pointer_axis_stop, .axis_discrete = &wl_pointer_axis_discrete, #if defined(WL_POINTER_AXIS_VALUE120_SINCE_VERSION) .axis_value120 = &wl_pointer_axis_value120, #endif }; static bool touch_to_scroll(struct seat *seat, struct terminal *term, wl_fixed_t surface_x, wl_fixed_t surface_y) { bool coord_updated = false; int y = wl_fixed_to_int(surface_y) * term->scale; int rows = (y - seat->mouse.y) / term->cell_height; if (rows != 0) { mouse_scroll(seat, -rows, WL_POINTER_AXIS_VERTICAL_SCROLL); seat->mouse.y += rows * term->cell_height; coord_updated = true; } int x = wl_fixed_to_int(surface_x) * term->scale; int cols = (x - seat->mouse.x) / term->cell_width; if (cols != 0) { mouse_scroll(seat, -cols, WL_POINTER_AXIS_HORIZONTAL_SCROLL); seat->mouse.x += cols * term->cell_width; coord_updated = true; } return coord_updated; } static void wl_touch_down(void *data, struct wl_touch *wl_touch, uint32_t serial, uint32_t time, struct wl_surface *surface, int32_t id, wl_fixed_t surface_x, wl_fixed_t surface_y) { struct seat *seat = data; if (seat->touch.state != TOUCH_STATE_IDLE) return; struct wl_window *win = wl_surface_get_user_data(surface); struct terminal *term = win->term; LOG_DBG("touch_down: touch=%p, x=%d, y=%d", (void *)wl_touch, wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); int x = wl_fixed_to_int(surface_x) * term->scale; int y = wl_fixed_to_int(surface_y) * term->scale; seat->mouse.x = x; seat->mouse.y = y; mouse_coord_pixel_to_cell(seat, term, x, y); seat->touch.state = TOUCH_STATE_HELD; seat->touch.serial = serial; seat->touch.time = time + term->conf->touch.long_press_delay; seat->touch.surface = surface; seat->touch.surface_kind = term_surface_kind(term, surface); seat->touch.id = id; } static void wl_touch_up(void *data, struct wl_touch *wl_touch, uint32_t serial, uint32_t time, int32_t id) { struct seat *seat = data; if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id) return; LOG_DBG("touch_up: touch=%p", (void *)wl_touch); struct wl_window *win = wl_surface_get_user_data(seat->touch.surface); struct terminal *term = win->term; struct terminal *old_term = seat->mouse_focus; enum term_surface old_active_surface = term->active_surface; seat->mouse_focus = term; term->active_surface = seat->touch.surface_kind; switch (seat->touch.state) { case TOUCH_STATE_HELD: wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT, WL_POINTER_BUTTON_STATE_PRESSED); /* fallthrough */ case TOUCH_STATE_DRAGGING: wl_pointer_button(seat, NULL, serial, time, BTN_LEFT, WL_POINTER_BUTTON_STATE_RELEASED); /* fallthrough */ case TOUCH_STATE_SCROLLING: term->active_surface = TERM_SURF_NONE; seat->touch.state = TOUCH_STATE_IDLE; break; case TOUCH_STATE_INHIBITED: case TOUCH_STATE_IDLE: BUG("Bad touch state: %d", seat->touch.state); break; } seat->mouse_focus = old_term; term->active_surface = old_active_surface; } static void wl_touch_motion(void *data, struct wl_touch *wl_touch, uint32_t time, int32_t id, wl_fixed_t surface_x, wl_fixed_t surface_y) { struct seat *seat = data; if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id) return; LOG_DBG("touch_motion: touch=%p, x=%d, y=%d", (void *)wl_touch, wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); struct wl_window *win = wl_surface_get_user_data(seat->touch.surface); struct terminal *term = win->term; struct terminal *old_term = seat->mouse_focus; enum term_surface old_active_surface = term->active_surface; seat->mouse_focus = term; term->active_surface = seat->touch.surface_kind; switch (seat->touch.state) { case TOUCH_STATE_HELD: if (time <= seat->touch.time && term->active_surface == TERM_SURF_GRID) { if (touch_to_scroll(seat, term, surface_x, surface_y)) seat->touch.state = TOUCH_STATE_SCROLLING; break; } else { wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT, WL_POINTER_BUTTON_STATE_PRESSED); seat->touch.state = TOUCH_STATE_DRAGGING; /* fallthrough */ } case TOUCH_STATE_DRAGGING: wl_pointer_motion(seat, NULL, time, surface_x, surface_y); break; case TOUCH_STATE_SCROLLING: touch_to_scroll(seat, term, surface_x, surface_y); break; case TOUCH_STATE_INHIBITED: case TOUCH_STATE_IDLE: BUG("Bad touch state: %d", seat->touch.state); break; } seat->mouse_focus = old_term; term->active_surface = old_active_surface; } static void wl_touch_frame(void *data, struct wl_touch *wl_touch) { } static void wl_touch_cancel(void *data, struct wl_touch *wl_touch) { struct seat *seat = data; if (seat->touch.state == TOUCH_STATE_INHIBITED) return; seat->touch.state = TOUCH_STATE_IDLE; } const struct wl_touch_listener touch_listener = { .down = wl_touch_down, .up = wl_touch_up, .motion = wl_touch_motion, .frame = wl_touch_frame, .cancel = wl_touch_cancel, }; foot-1.21.0/input.h000066400000000000000000000023721476600145200140630ustar00rootroot00000000000000#pragma once #include #include #include "cursor-shape.h" #include "misc.h" #include "wayland.h" /* * Custom defines for mouse wheel left/right buttons. * * Libinput does not define these. On Wayland, all scroll events (both * vertical and horizontal) are reported not as buttons, as 'axis' * events. * * Libinput _does_ define BTN_BACK and BTN_FORWARD, which is * what we use for vertical scroll events. But for horizontal scroll * events, there aren't any pre-defined mouse buttons. * * Mouse buttons are in the range 0x110 - 0x11f, with joystick defines * starting at 0x120. */ #define BTN_WHEEL_BACK 0x11c #define BTN_WHEEL_FORWARD 0x11d #define BTN_WHEEL_LEFT 0x11e #define BTN_WHEEL_RIGHT 0x11f extern const struct wl_keyboard_listener keyboard_listener; extern const struct wl_pointer_listener pointer_listener; extern const struct wl_touch_listener touch_listener; void input_repeat(struct seat *seat, uint32_t key); void get_current_modifiers(const struct seat *seat, xkb_mod_mask_t *effective, xkb_mod_mask_t *consumed, uint32_t key, bool filter_locked); enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y); foot-1.21.0/key-binding.c000066400000000000000000000413601476600145200151170ustar00rootroot00000000000000#include "key-binding.h" #include #define LOG_MODULE "key-binding" #define LOG_ENABLE_DBG 0 #include "log.h" #include "config.h" #include "debug.h" #include "terminal.h" #include "util.h" #include "wayland.h" #include "xmalloc.h" struct key_set { struct key_binding_set public; const struct config *conf; const struct seat *seat; size_t conf_ref_count; }; typedef tll(struct key_set) bind_set_list_t; struct key_binding_manager { struct key_set *last_used_set; bind_set_list_t binding_sets; }; static void load_keymap(struct key_set *set); static void unload_keymap(struct key_set *set); struct key_binding_manager * key_binding_manager_new(void) { struct key_binding_manager *mgr = xcalloc(1, sizeof(*mgr)); return mgr; } void key_binding_manager_destroy(struct key_binding_manager *mgr) { xassert(tll_length(mgr->binding_sets) == 0); free(mgr); } void key_binding_new_for_seat(struct key_binding_manager *mgr, const struct seat *seat) { #if defined(_DEBUG) tll_foreach(mgr->binding_sets, it) xassert(it->item.seat != seat); #endif tll_foreach(seat->wayl->terms, it) { struct key_set set = { .public = { .key = tll_init(), .search = tll_init(), .url = tll_init(), .mouse = tll_init(), }, .conf = it->item->conf, .seat = seat, .conf_ref_count = 1, }; tll_push_back(mgr->binding_sets, set); LOG_DBG("new (seat): set=%p, seat=%p, conf=%p, ref-count=1", (void *)&tll_back(mgr->binding_sets), (void *)set.seat, (void *)set.conf); load_keymap(&tll_back(mgr->binding_sets)); } LOG_DBG("new (seat): total number of sets: %zu", tll_length(mgr->binding_sets)); } void key_binding_new_for_conf(struct key_binding_manager *mgr, const struct wayland *wayl, const struct config *conf) { tll_foreach(wayl->seats, it) { struct seat *seat = &it->item; struct key_set *existing = (struct key_set *)key_binding_for(mgr, conf, seat); if (existing != NULL) { existing->conf_ref_count++; continue; } struct key_set set = { .public = { .key = tll_init(), .search = tll_init(), .url = tll_init(), .mouse = tll_init(), }, .conf = conf, .seat = seat, .conf_ref_count = 1, }; tll_push_back(mgr->binding_sets, set); load_keymap(&tll_back(mgr->binding_sets)); /* Chances are high this set will be requested next */ mgr->last_used_set = &tll_back(mgr->binding_sets); LOG_DBG("new (conf): set=%p, seat=%p, conf=%p, ref-count=1", (void *)&tll_back(mgr->binding_sets), (void *)set.seat, (void *)set.conf); } LOG_DBG("new (conf): total number of sets: %zu", tll_length(mgr->binding_sets)); } struct key_binding_set * NOINLINE key_binding_for(struct key_binding_manager *mgr, const struct config *conf, const struct seat *seat) { struct key_set *last_used = mgr->last_used_set; if (last_used != NULL && last_used->conf == conf && last_used->seat == seat) { // LOG_DBG("lookup: last used"); return &last_used->public; } tll_foreach(mgr->binding_sets, it) { struct key_set *set = &it->item; if (set->conf != conf) continue; if (set->seat != seat) continue; #if 0 LOG_DBG("lookup: set=%p, seat=%p, conf=%p, ref-count=%zu", (void *)set, (void *)seat, (void *)conf, set->conf_ref_count); #endif mgr->last_used_set = set; return &set->public; } return NULL; } static void key_binding_set_destroy(struct key_binding_manager *mgr, struct key_set *set) { unload_keymap(set); if (mgr->last_used_set == set) mgr->last_used_set = NULL; /* Note: caller must remove from binding_sets */ } void key_binding_remove_seat(struct key_binding_manager *mgr, const struct seat *seat) { tll_foreach(mgr->binding_sets, it) { struct key_set *set = &it->item; if (set->seat != seat) continue; key_binding_set_destroy(mgr, set); tll_remove(mgr->binding_sets, it); LOG_DBG("remove seat: set=%p, seat=%p, total number of sets: %zu", (void *)set, (void *)seat, tll_length(mgr->binding_sets)); } LOG_DBG("remove seat: total number of sets: %zu", tll_length(mgr->binding_sets)); } void key_binding_unref(struct key_binding_manager *mgr, const struct config *conf) { tll_foreach(mgr->binding_sets, it) { struct key_set *set = &it->item; if (set->conf != conf) continue; xassert(set->conf_ref_count > 0); if (--set->conf_ref_count == 0) { LOG_DBG("unref conf: set=%p, seat=%p, conf=%p", (void *)set, (void *)set->seat, (void *)conf); key_binding_set_destroy(mgr, set); tll_remove(mgr->binding_sets, it); } } LOG_DBG("unref conf: total number of sets: %zu", tll_length(mgr->binding_sets)); } static xkb_keycode_list_t key_codes_for_xkb_sym(struct xkb_keymap *keymap, xkb_keysym_t sym) { xkb_keycode_list_t key_codes = tll_init(); /* * Find all key codes that map to this symbol. * * This allows us to match bindings in other layouts * too. */ struct xkb_state *state = xkb_state_new(keymap); for (xkb_keycode_t code = xkb_keymap_min_keycode(keymap); code <= xkb_keymap_max_keycode(keymap); code++) { if (xkb_state_key_get_one_sym(state, code) == sym) tll_push_back(key_codes, code); } xkb_state_unref(state); return key_codes; } static xkb_keysym_t maybe_repair_key_combo(const struct seat *seat, xkb_keysym_t sym, xkb_mod_mask_t mods) { /* * Detect combos containing a shifted symbol and the corresponding * modifier, and replace the shifted symbol with its unshifted * variant. * * For example, the combo is "Control+Shift+U". In this case, * Shift is the modifier used to "shift" 'u' to 'U', after which * 'Shift' will have been "consumed". Since we filter out consumed * modifiers when matching key combos, this key combo will never * trigger (we will never be able to match the 'Shift' modifier). * * There are two correct variants of the above key combo: * - "Control+U" (upper case 'U') * - "Control+Shift+u" (lower case 'u') * * What we do here is, for each key *code*, check if there are any * (shifted) levels where it produces 'sym'. If there are, check * *which* sets of modifiers are needed to produce it, and compare * with 'mods'. * * If there is at least one common modifier, it means 'sym' is a * "shifted" symbol, with the corresponding shifting modifier * explicitly included in the key combo. I.e. the key combo will * never trigger. * * We then proceed and "repair" the key combo by replacing 'sym' * with the corresponding unshifted symbol. * * To reduce the noise, we ignore all key codes where the shifted * symbol is the same as the unshifted symbol. */ for (xkb_keycode_t code = xkb_keymap_min_keycode(seat->kbd.xkb_keymap); code <= xkb_keymap_max_keycode(seat->kbd.xkb_keymap); code++) { xkb_layout_index_t layout_idx = xkb_state_key_get_layout(seat->kbd.xkb_state, code); /* Get all unshifted symbols for this key */ const xkb_keysym_t *base_syms = NULL; size_t base_count = xkb_keymap_key_get_syms_by_level( seat->kbd.xkb_keymap, code, layout_idx, 0, &base_syms); if (base_count == 0 || sym == base_syms[0]) { /* No unshifted symbols, or unshifted symbol is same as 'sym' */ continue; } /* Name of the unshifted symbol, for logging */ char base_name[100]; xkb_keysym_get_name(base_syms[0], base_name, sizeof(base_name)); /* Iterate all shift levels */ for (xkb_level_index_t level_idx = 1; level_idx < xkb_keymap_num_levels_for_key( seat->kbd.xkb_keymap, code, layout_idx); level_idx++) { /* Get all symbols for current shift level */ const xkb_keysym_t *shifted_syms = NULL; size_t shifted_count = xkb_keymap_key_get_syms_by_level( seat->kbd.xkb_keymap, code, layout_idx, level_idx, &shifted_syms); for (size_t i = 0; i < shifted_count; i++) { if (shifted_syms[i] != sym) continue; /* Get modifier sets that produces the current shift level */ xkb_mod_mask_t mod_masks[16]; size_t mod_mask_count = xkb_keymap_key_get_mods_for_level( seat->kbd.xkb_keymap, code, layout_idx, level_idx, mod_masks, ALEN(mod_masks)); /* Check if key combo's modifier set intersects */ for (size_t j = 0; j < mod_mask_count; j++) { if ((mod_masks[j] & mods) != mod_masks[j]) continue; char combo[64] = {0}; for (int k = 0; k < sizeof(xkb_mod_mask_t) * 8; k++) { if (!(mods & (1u << k))) continue; const char *mod_name = xkb_keymap_mod_get_name( seat->kbd.xkb_keymap, k); strcat(combo, mod_name); strcat(combo, "+"); } size_t len = strlen(combo); xkb_keysym_get_name( sym, &combo[len], sizeof(combo) - len); LOG_WARN( "%s: combo with both explicit modifier and shifted symbol " "(level=%d, mod-mask=0x%08x), " "replacing with %s", combo, level_idx, mod_masks[j], base_name); /* Replace with unshifted symbol */ return base_syms[0]; } } } } return sym; } static int key_cmp(struct key_binding a, struct key_binding b) { xassert(a.type == b.type); /* * Sort bindings such that bindings with the same symbol are * sorted with the binding having the most modifiers comes first. * * This fixes an issue where the "wrong" key binding are triggered * when used with "consumed" modifiers. * * For example: if Control+BackSpace is bound before * Control+Shift+BackSpace, then the latter binding is never * triggered. * * Why? Because Shift is a consumed modifier. This means * Control+BackSpace is "the same" as Control+Shift+BackSpace. * * By sorting bindings with more modifiers first, we work around * the problem. But note that it is *just* a workaround, and I'm * not confident there aren't cases where it doesn't work. * * See https://codeberg.org/dnkl/foot/issues/1280 */ const int a_mod_count = __builtin_popcount(a.mods); const int b_mod_count = __builtin_popcount(b.mods); switch (a.type) { case KEY_BINDING: if (a.k.sym != b.k.sym) return b.k.sym - a.k.sym; return b_mod_count - a_mod_count; case MOUSE_BINDING: { if (a.m.button != b.m.button) return b.m.button - a.m.button; if (a_mod_count != b_mod_count) return b_mod_count - a_mod_count; return b.m.count - a.m.count; } } BUG("invalid key binding type"); return 0; } static void NOINLINE sort_binding_list(key_binding_list_t *list) { tll_sort(*list, key_cmp); } static xkb_mod_mask_t mods_to_mask(const struct seat *seat, const config_modifier_list_t *mods) { xkb_mod_mask_t mask = 0; tll_foreach(*mods, it) { 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; } mask |= 1 << idx; } return mask; } static void NOINLINE convert_key_binding(struct key_set *set, const struct config_key_binding *conf_binding, key_binding_list_t *bindings) { const struct seat *seat = set->seat; xkb_mod_mask_t mods = mods_to_mask(seat, &conf_binding->modifiers); xkb_keysym_t sym = maybe_repair_key_combo(seat, conf_binding->k.sym, mods); struct key_binding binding = { .type = KEY_BINDING, .action = conf_binding->action, .aux = &conf_binding->aux, .mods = mods, .k = { .sym = sym, .key_codes = key_codes_for_xkb_sym(seat->kbd.xkb_keymap, sym), }, }; tll_push_back(*bindings, binding); sort_binding_list(bindings); } static void convert_key_bindings(struct key_set *set) { const struct config *conf = set->conf; for (size_t i = 0; i < conf->bindings.key.count; i++) { const struct config_key_binding *binding = &conf->bindings.key.arr[i]; convert_key_binding(set, binding, &set->public.key); } } static void convert_search_bindings(struct key_set *set) { const struct config *conf = set->conf; for (size_t i = 0; i < conf->bindings.search.count; i++) { const struct config_key_binding *binding = &conf->bindings.search.arr[i]; convert_key_binding(set, binding, &set->public.search); } } static void convert_url_bindings(struct key_set *set) { const struct config *conf = set->conf; for (size_t i = 0; i < conf->bindings.url.count; i++) { const struct config_key_binding *binding = &conf->bindings.url.arr[i]; convert_key_binding(set, binding, &set->public.url); } } static void convert_mouse_binding(struct key_set *set, const struct config_key_binding *conf_binding) { struct key_binding binding = { .type = MOUSE_BINDING, .action = conf_binding->action, .aux = &conf_binding->aux, .mods = mods_to_mask(set->seat, &conf_binding->modifiers), .m = { .button = conf_binding->m.button, .count = conf_binding->m.count, }, }; tll_push_back(set->public.mouse, binding); sort_binding_list(&set->public.mouse); } static void convert_mouse_bindings(struct key_set *set) { const struct config *conf = set->conf; for (size_t i = 0; i < conf->bindings.mouse.count; i++) { const struct config_key_binding *binding = &conf->bindings.mouse.arr[i]; convert_mouse_binding(set, binding); } } static void NOINLINE load_keymap(struct key_set *set) { LOG_DBG("load keymap: set=%p, seat=%p, conf=%p", (void *)set, (void *)set->seat, (void *)set->conf); if (set->seat->kbd.xkb_state == NULL || set->seat->kbd.xkb_keymap == NULL) { LOG_DBG("no XKB keymap"); return; } convert_key_bindings(set); convert_search_bindings(set); convert_url_bindings(set); convert_mouse_bindings(set); set->public.selection_overrides = mods_to_mask( set->seat, &set->conf->mouse.selection_override_modifiers); } void key_binding_load_keymap(struct key_binding_manager *mgr, const struct seat *seat) { tll_foreach(mgr->binding_sets, it) { struct key_set *set = &it->item; if (set->seat == seat) load_keymap(set); } } static void NOINLINE key_bindings_destroy(key_binding_list_t *bindings) { tll_foreach(*bindings, it) { struct key_binding *bind = &it->item; switch (bind->type) { case KEY_BINDING: tll_free(it->item.k.key_codes); break; case MOUSE_BINDING: break; } tll_remove(*bindings, it); } } static void NOINLINE unload_keymap(struct key_set *set) { key_bindings_destroy(&set->public.key); key_bindings_destroy(&set->public.search); key_bindings_destroy(&set->public.url); key_bindings_destroy(&set->public.mouse); set->public.selection_overrides = 0; } void key_binding_unload_keymap(struct key_binding_manager *mgr, const struct seat *seat) { tll_foreach(mgr->binding_sets, it) { struct key_set *set = &it->item; if (set->seat != seat) continue; LOG_DBG("unload keymap: set=%p, seat=%p, conf=%p", (void *)set, (void *)seat, (void *)set->conf); unload_keymap(set); } } foot-1.21.0/key-binding.h000066400000000000000000000117471476600145200151320ustar00rootroot00000000000000#pragma once #include #include #include #include "config.h" enum bind_action_normal { BIND_ACTION_NONE, BIND_ACTION_NOOP, BIND_ACTION_SCROLLBACK_UP_PAGE, BIND_ACTION_SCROLLBACK_UP_HALF_PAGE, BIND_ACTION_SCROLLBACK_UP_LINE, BIND_ACTION_SCROLLBACK_DOWN_PAGE, BIND_ACTION_SCROLLBACK_DOWN_HALF_PAGE, BIND_ACTION_SCROLLBACK_DOWN_LINE, BIND_ACTION_SCROLLBACK_HOME, BIND_ACTION_SCROLLBACK_END, BIND_ACTION_CLIPBOARD_COPY, BIND_ACTION_CLIPBOARD_PASTE, BIND_ACTION_PRIMARY_PASTE, BIND_ACTION_SEARCH_START, BIND_ACTION_FONT_SIZE_UP, BIND_ACTION_FONT_SIZE_DOWN, BIND_ACTION_FONT_SIZE_RESET, BIND_ACTION_SPAWN_TERMINAL, BIND_ACTION_MINIMIZE, BIND_ACTION_MAXIMIZE, BIND_ACTION_FULLSCREEN, BIND_ACTION_PIPE_SCROLLBACK, BIND_ACTION_PIPE_VIEW, BIND_ACTION_PIPE_SELECTED, BIND_ACTION_PIPE_COMMAND_OUTPUT, BIND_ACTION_SHOW_URLS_COPY, BIND_ACTION_SHOW_URLS_LAUNCH, BIND_ACTION_SHOW_URLS_PERSISTENT, BIND_ACTION_TEXT_BINDING, BIND_ACTION_PROMPT_PREV, BIND_ACTION_PROMPT_NEXT, BIND_ACTION_UNICODE_INPUT, BIND_ACTION_QUIT, BIND_ACTION_REGEX_LAUNCH, BIND_ACTION_REGEX_COPY, /* 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_REGEX_COPY + 1, BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1, }; enum bind_action_search { BIND_ACTION_SEARCH_NONE, BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE, BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE, BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE, BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE, BIND_ACTION_SEARCH_SCROLLBACK_HOME, BIND_ACTION_SEARCH_SCROLLBACK_END, BIND_ACTION_SEARCH_CANCEL, BIND_ACTION_SEARCH_COMMIT, BIND_ACTION_SEARCH_FIND_PREV, BIND_ACTION_SEARCH_FIND_NEXT, BIND_ACTION_SEARCH_EDIT_LEFT, BIND_ACTION_SEARCH_EDIT_LEFT_WORD, BIND_ACTION_SEARCH_EDIT_RIGHT, BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, BIND_ACTION_SEARCH_EDIT_HOME, BIND_ACTION_SEARCH_EDIT_END, BIND_ACTION_SEARCH_DELETE_PREV, BIND_ACTION_SEARCH_DELETE_PREV_WORD, BIND_ACTION_SEARCH_DELETE_NEXT, BIND_ACTION_SEARCH_DELETE_NEXT_WORD, BIND_ACTION_SEARCH_DELETE_TO_START, BIND_ACTION_SEARCH_DELETE_TO_END, BIND_ACTION_SEARCH_EXTEND_CHAR, BIND_ACTION_SEARCH_EXTEND_WORD, BIND_ACTION_SEARCH_EXTEND_WORD_WS, BIND_ACTION_SEARCH_EXTEND_LINE_DOWN, BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR, BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD, BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS, BIND_ACTION_SEARCH_EXTEND_LINE_UP, BIND_ACTION_SEARCH_CLIPBOARD_PASTE, BIND_ACTION_SEARCH_PRIMARY_PASTE, BIND_ACTION_SEARCH_UNICODE_INPUT, BIND_ACTION_SEARCH_COUNT, }; enum bind_action_url { BIND_ACTION_URL_NONE, BIND_ACTION_URL_CANCEL, BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL, BIND_ACTION_URL_COUNT, }; typedef tll(xkb_keycode_t) xkb_keycode_list_t; struct key_binding { enum key_binding_type type; int action; /* enum bind_action_* */ xkb_mod_mask_t mods; union { struct { xkb_keysym_t sym; xkb_keycode_list_t key_codes; } k; struct { uint32_t button; int count; } m; }; const struct binding_aux *aux; }; typedef tll(struct key_binding) key_binding_list_t; struct terminal; struct seat; struct wayland; struct key_binding_set { key_binding_list_t key; key_binding_list_t search; key_binding_list_t url; key_binding_list_t mouse; xkb_mod_mask_t selection_overrides; }; struct key_binding_manager; struct key_binding_manager *key_binding_manager_new(void); void key_binding_manager_destroy(struct key_binding_manager *mgr); void key_binding_new_for_seat( struct key_binding_manager *mgr, const struct seat *seat); void key_binding_new_for_conf( struct key_binding_manager *mgr, const struct wayland *wayl, const struct config *conf); /* Returns the set of key bindings associated with this seat/conf pair */ struct key_binding_set *key_binding_for( struct key_binding_manager *mgr, const struct config *conf, const struct seat *seat); /* Remove all key bindings tied to the specified seat */ void key_binding_remove_seat( struct key_binding_manager *mgr, const struct seat *seat); void key_binding_unref( struct key_binding_manager *mgr, const struct config *conf); void key_binding_load_keymap( struct key_binding_manager *mgr, const struct seat *seat); void key_binding_unload_keymap( struct key_binding_manager *mgr, const struct seat *seat); foot-1.21.0/keymap.h000066400000000000000000000670571476600145200142250ustar00rootroot00000000000000#pragma once #include #include "terminal.h" enum modifier { MOD_NONE = 0x0, MOD_ANY = 0x1, MOD_SHIFT = 0x2, MOD_ALT = 0x4, MOD_CTRL = 0x8, MOD_META = 0x10, MOD_MODIFY_OTHER_KEYS_STATE1 = 0x20, MOD_MODIFY_OTHER_KEYS_STATE2 = 0x40, }; struct key_data { enum modifier modifiers; enum cursor_keys cursor_keys_mode; enum keypad_keys keypad_keys_mode; const char *seq; }; static const struct key_data key_escape[] = { {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;27~"}, {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\033"}, {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;27~"}, {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;5;27~"}, {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;27~"}, {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;27~"}, {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;27~"}, {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;27~"}, {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;27~"}, {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;27~"}, {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;27~"}, {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;27~"}, {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;27~"}, {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;27~"}, {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;27~"}, {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033"}, }; static const struct key_data key_return[] = { {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;13~"}, {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\r"}, {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;3;13~"}, {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;13~"}, {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;5;13~"}, {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;13~"}, {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;13~"}, {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;13~"}, {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;13~"}, {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;13~"}, {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;13~"}, {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;13~"}, {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;13~"}, {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;13~"}, {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;13~"}, {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;13~"}, {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\r"}, }; /* Tab isn't covered by the regular "modifyOtherKeys" handling */ static const struct key_data key_tab[] = { {MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[Z"}, {MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;9~"}, {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\t"}, {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;3;9~"}, {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;9~"}, {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;5;9~"}, {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;9~"}, {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;9~"}, {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;9~"}, {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;9~"}, {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;9~"}, {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;9~"}, {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;9~"}, {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;9~"}, {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;9~"}, {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;9~"}, {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;9~"}, {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\t"}, }; /* * Shift+Tab produces ISO_Left_Tab * * However, all combos (except Shift+Tab) acts as if we pressed * mods+shift+tab. */ static const struct key_data key_iso_left_tab[] = { {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;9~"}, {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;9~"}, {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;9~"}, {MOD_SHIFT | MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;9~"}, {MOD_SHIFT | MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;9~"}, {MOD_SHIFT | MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;9~"}, {MOD_SHIFT | MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;9~"}, {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[Z"}, }; static const struct key_data key_backspace[] = { {MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"}, {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"}, {MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"}, {MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"}, {MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"}, {MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"}, {MOD_META | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"}, {MOD_META | MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"}, {MOD_META | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"}, {MOD_META | MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"}, {MOD_META | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"}, {MOD_META | MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"}, {MOD_META | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"}, {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"}, {MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;127~"}, {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;3;127~"}, {MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;127~"}, {MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;8~"}, {MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;8~"}, {MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;8~"}, {MOD_META | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;127~"}, {MOD_META | MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;127~"}, {MOD_META | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;127~"}, {MOD_META | MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;127~"}, {MOD_META | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;8~"}, {MOD_META | MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;8~"}, {MOD_META | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;8~"}, {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;8~"}, {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"}, {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"}, }; #define DEFAULT_MODS_FOR_SINGLE(sym) \ {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2"#sym}, \ {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;3"#sym}, \ {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;4"#sym}, \ {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5"#sym}, \ {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;6"#sym}, \ {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;7"#sym}, \ {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;8"#sym}, \ {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;9"#sym}, \ {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;10"#sym}, \ {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;11"#sym}, \ {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;12"#sym}, \ {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;13"#sym}, \ {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;14"#sym}, \ {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;15"#sym}, \ {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;16"#sym} #define DEFAULT_MODS_FOR_TILDE(sym) \ {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";2~"}, \ {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";3~"}, \ {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";4~"}, \ {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";5~"}, \ {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";6~"}, \ {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";7~"}, \ {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";8~"}, \ {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";9~"}, \ {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";10~"}, \ {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";11~"}, \ {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";12~"}, \ {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";13~"}, \ {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";14~"}, \ {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";15~"}, \ {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";16~"} static const struct key_data key_up[] = { DEFAULT_MODS_FOR_SINGLE(A), {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OA"}, {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[A"}, }; static const struct key_data key_down[] = { DEFAULT_MODS_FOR_SINGLE(B), {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OB"}, {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[B"}, }; static const struct key_data key_right[] = { DEFAULT_MODS_FOR_SINGLE(C), {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OC"}, {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[C"}, }; static const struct key_data key_left[] = { DEFAULT_MODS_FOR_SINGLE(D), {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OD"}, {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[D"}, }; static const struct key_data key_home[] = { DEFAULT_MODS_FOR_SINGLE(H), {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OH"}, {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[H"}, }; static const struct key_data key_end[] = { DEFAULT_MODS_FOR_SINGLE(F), {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OF"}, {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[F"}, }; static const struct key_data key_insert[] = { DEFAULT_MODS_FOR_TILDE(2), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[2~"}, }; static const struct key_data key_delete[] = { DEFAULT_MODS_FOR_TILDE(3), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[3~"}, }; static const struct key_data key_pageup[] = { DEFAULT_MODS_FOR_TILDE(5), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[5~"}, }; static const struct key_data key_pagedown[] = { DEFAULT_MODS_FOR_TILDE(6), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[6~"}, }; static const struct key_data key_f1[] = { DEFAULT_MODS_FOR_SINGLE(P), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OP"}, }; static const struct key_data key_f2[] = { DEFAULT_MODS_FOR_SINGLE(Q), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OQ"}, }; static const struct key_data key_f3[] = { DEFAULT_MODS_FOR_SINGLE(R), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OR"}, }; static const struct key_data key_f4[] = { DEFAULT_MODS_FOR_SINGLE(S), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OS"}, }; static const struct key_data key_f5[] = { DEFAULT_MODS_FOR_TILDE(15), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[15~"}, }; static const struct key_data key_f6[] = { DEFAULT_MODS_FOR_TILDE(17), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[17~"}, }; static const struct key_data key_f7[] = { DEFAULT_MODS_FOR_TILDE(18), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[18~"}, }; static const struct key_data key_f8[] = { DEFAULT_MODS_FOR_TILDE(19), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[19~"}, }; static const struct key_data key_f9[] = { DEFAULT_MODS_FOR_TILDE(20), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[20~"}, }; static const struct key_data key_f10[] = { DEFAULT_MODS_FOR_TILDE(21), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[21~"}, }; static const struct key_data key_f11[] = { DEFAULT_MODS_FOR_TILDE(23), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[23~"}, }; static const struct key_data key_f12[] = { DEFAULT_MODS_FOR_TILDE(24), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[24~"}, }; static const struct key_data key_f13[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2P"}}; static const struct key_data key_f14[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2Q"}}; static const struct key_data key_f15[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2R"}}; static const struct key_data key_f16[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2S"}}; static const struct key_data key_f17[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[15;2~"}}; static const struct key_data key_f18[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[17;2~"}}; static const struct key_data key_f19[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[18;2~"}}; static const struct key_data key_f20[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[19;2~"}}; static const struct key_data key_f21[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[20;2~"}}; static const struct key_data key_f22[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[21;2~"}}; static const struct key_data key_f23[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[23;2~"}}; static const struct key_data key_f24[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[24;2~"}}; static const struct key_data key_f25[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5P"}}; static const struct key_data key_f26[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5Q"}}; static const struct key_data key_f27[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5R"}}; static const struct key_data key_f28[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5S"}}; static const struct key_data key_f29[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[15;5~"}}; static const struct key_data key_f30[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[17;5~"}}; static const struct key_data key_f31[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[18;5~"}}; static const struct key_data key_f32[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[19;5~"}}; static const struct key_data key_f33[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[20;5~"}}; static const struct key_data key_f34[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[21;5~"}}; static const struct key_data key_f35[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[23;5~"}}; /* Keypad keys don't map shift */ #undef DEFAULT_MODS_FOR_SINGLE #undef DEFAULT_MODS_FOR_TILDE #define DEFAULT_MODS_FOR_SINGLE(sym) \ {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;3"#sym}, \ {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5"#sym}, \ {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;7"#sym}, \ {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;9"#sym}, \ {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;11"#sym}, \ {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;13"#sym}, \ {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;15"#sym} #define DEFAULT_MODS_FOR_TILDE(sym) \ {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";3~"}, \ {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";5~"}, \ {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";7~"}, \ {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";9~"}, \ {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";11~"}, \ {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";13~"}, \ {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";15~"} static const struct key_data key_kp_up[] = { DEFAULT_MODS_FOR_SINGLE(A), {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[A"}, {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OA"}, }; static const struct key_data key_kp_down[] = { DEFAULT_MODS_FOR_SINGLE(B), {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[B"}, {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OB"}, }; static const struct key_data key_kp_right[] = { DEFAULT_MODS_FOR_SINGLE(C), {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[C"}, {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OC"}, }; static const struct key_data key_kp_left[] = { DEFAULT_MODS_FOR_SINGLE(D), {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[D"}, {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OD"}, }; static const struct key_data key_kp_begin[] = { DEFAULT_MODS_FOR_SINGLE(E), {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[E"}, {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OE"}, }; static const struct key_data key_kp_home[] = { DEFAULT_MODS_FOR_SINGLE(H), {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[H"}, {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OH"}, }; static const struct key_data key_kp_end[] = { DEFAULT_MODS_FOR_SINGLE(F), {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[F"}, {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OF"}, }; static const struct key_data key_kp_insert[] = { DEFAULT_MODS_FOR_TILDE(2), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[2~"}, }; static const struct key_data key_kp_delete[] = { DEFAULT_MODS_FOR_TILDE(3), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[3~"}, }; static const struct key_data key_kp_pageup[] = { DEFAULT_MODS_FOR_TILDE(5), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[5~"}, }; static const struct key_data key_kp_pagedown[] = { DEFAULT_MODS_FOR_TILDE(6), {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[6~"}, }; #undef DEFAULT_MODS_FOR_SINGLE #undef DEFAULT_MODS_FOR_TILDE #define DEFAULT_MODS_FOR_KP(sym) \ {MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O"#sym}, \ {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O2"#sym}, \ {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O3"#sym}, \ {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O4"#sym}, \ {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O5"#sym}, \ {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O6"#sym}, \ {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O7"#sym}, \ {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O8"#sym}, \ {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O9"#sym}, \ {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O10"#sym}, \ {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O11"#sym}, \ {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O12"#sym}, \ {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O13"#sym}, \ {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O14"#sym}, \ {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O15"#sym}, \ {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O16"#sym} static const struct key_data key_kp_enter[] = {DEFAULT_MODS_FOR_KP(M)}; static const struct key_data key_kp_divide[] = {DEFAULT_MODS_FOR_KP(o)}; static const struct key_data key_kp_multiply[] = {DEFAULT_MODS_FOR_KP(j)}; static const struct key_data key_kp_subtract[] = {DEFAULT_MODS_FOR_KP(m)}; static const struct key_data key_kp_add[] = {DEFAULT_MODS_FOR_KP(k)}; static const struct key_data key_kp_separator[] = {DEFAULT_MODS_FOR_KP(l)}; static const struct key_data key_kp_decimal[] = {DEFAULT_MODS_FOR_KP(n)}; static const struct key_data key_kp_0[] = {DEFAULT_MODS_FOR_KP(p)}; static const struct key_data key_kp_1[] = {DEFAULT_MODS_FOR_KP(q)}; static const struct key_data key_kp_2[] = {DEFAULT_MODS_FOR_KP(r)}; static const struct key_data key_kp_3[] = {DEFAULT_MODS_FOR_KP(s)}; static const struct key_data key_kp_4[] = {DEFAULT_MODS_FOR_KP(t)}; static const struct key_data key_kp_5[] = {DEFAULT_MODS_FOR_KP(u)}; static const struct key_data key_kp_6[] = {DEFAULT_MODS_FOR_KP(v)}; static const struct key_data key_kp_7[] = {DEFAULT_MODS_FOR_KP(w)}; static const struct key_data key_kp_8[] = {DEFAULT_MODS_FOR_KP(x)}; static const struct key_data key_kp_9[] = {DEFAULT_MODS_FOR_KP(y)}; #undef DEFAULT_MODS_FOR_KP foot-1.21.0/kitty-keymap.h000066400000000000000000000131751476600145200153570ustar00rootroot00000000000000#pragma once #include #include #include struct kitty_key_data { xkb_keysym_t sym; uint16_t key; uint8_t final:7; bool is_modifier:1; } __attribute__((packed)); _Static_assert(sizeof(struct kitty_key_data) == 7, "bad size"); /* Note! *Must* Be kept sorted (on 'sym') */ static const struct kitty_key_data kitty_keymap[] = { {XKB_KEY_ISO_Level3_Shift, 57453, 'u', true}, {XKB_KEY_ISO_Level5_Shift, 57454, 'u', true}, {XKB_KEY_ISO_Left_Tab, 9, 'u', false}, {XKB_KEY_BackSpace, 127, 'u', false}, {XKB_KEY_Tab, 9, 'u', false}, {XKB_KEY_Return, 13, 'u', false}, {XKB_KEY_Pause, 57362, 'u', false}, {XKB_KEY_Scroll_Lock, 57359, 'u', false}, {XKB_KEY_Escape, 27, 'u', false}, {XKB_KEY_Home, 1, 'H', false}, {XKB_KEY_Left, 1, 'D', false}, {XKB_KEY_Up, 1, 'A', false}, {XKB_KEY_Right, 1, 'C', false}, {XKB_KEY_Down, 1, 'B', false}, {XKB_KEY_Prior, 5, '~', false}, {XKB_KEY_Next, 6, '~', false}, {XKB_KEY_End, 1, 'F', false}, {XKB_KEY_Print, 57361, 'u', false}, {XKB_KEY_Insert, 2, '~', false}, {XKB_KEY_Menu, 57363, 'u', false}, {XKB_KEY_Num_Lock, 57360, 'u', true}, {XKB_KEY_KP_Enter, 57414, 'u', false}, {XKB_KEY_KP_Home, 57423, 'u', false}, {XKB_KEY_KP_Left, 57417, 'u', false}, {XKB_KEY_KP_Up, 57419, 'u', false}, {XKB_KEY_KP_Right, 57418, 'u', false}, {XKB_KEY_KP_Down, 57420, 'u', false}, {XKB_KEY_KP_Prior, 57421, 'u', false}, {XKB_KEY_KP_Next, 57422, 'u', false}, {XKB_KEY_KP_End, 57424, 'u', false}, {XKB_KEY_KP_Begin, 1, 'E', false}, {XKB_KEY_KP_Insert, 57425, 'u', false}, {XKB_KEY_KP_Delete, 57426, 'u', false}, {XKB_KEY_KP_Multiply, 57411, 'u', false}, {XKB_KEY_KP_Add, 57413, 'u', false}, {XKB_KEY_KP_Separator, 57416, 'u', false}, {XKB_KEY_KP_Subtract, 57412, 'u', false}, {XKB_KEY_KP_Decimal, 57409, 'u', false}, {XKB_KEY_KP_Divide, 57410, 'u', false}, {XKB_KEY_KP_0, 57399, 'u', false}, {XKB_KEY_KP_1, 57400, 'u', false}, {XKB_KEY_KP_2, 57401, 'u', false}, {XKB_KEY_KP_3, 57402, 'u', false}, {XKB_KEY_KP_4, 57403, 'u', false}, {XKB_KEY_KP_5, 57404, 'u', false}, {XKB_KEY_KP_6, 57405, 'u', false}, {XKB_KEY_KP_7, 57406, 'u', false}, {XKB_KEY_KP_8, 57407, 'u', false}, {XKB_KEY_KP_9, 57408, 'u', false}, {XKB_KEY_KP_Equal, 57415, 'u', false}, {XKB_KEY_F1, 1, 'P', false}, {XKB_KEY_F2, 1, 'Q', false}, {XKB_KEY_F3, 13, '~', false}, {XKB_KEY_F4, 1, 'S', false}, {XKB_KEY_F5, 15, '~', false}, {XKB_KEY_F6, 17, '~', false}, {XKB_KEY_F7, 18, '~', false}, {XKB_KEY_F8, 19, '~', false}, {XKB_KEY_F9, 20, '~', false}, {XKB_KEY_F10, 21, '~', false}, {XKB_KEY_F11, 23, '~', false}, {XKB_KEY_F12, 24, '~', false}, {XKB_KEY_F13, 57376, 'u', false}, {XKB_KEY_F14, 57377, 'u', false}, {XKB_KEY_F15, 57378, 'u', false}, {XKB_KEY_F16, 57379, 'u', false}, {XKB_KEY_F17, 57380, 'u', false}, {XKB_KEY_F18, 57381, 'u', false}, {XKB_KEY_F19, 57382, 'u', false}, {XKB_KEY_F20, 57383, 'u', false}, {XKB_KEY_F21, 57384, 'u', false}, {XKB_KEY_F22, 57385, 'u', false}, {XKB_KEY_F23, 57386, 'u', false}, {XKB_KEY_F24, 57387, 'u', false}, {XKB_KEY_F25, 57388, 'u', false}, {XKB_KEY_F26, 57389, 'u', false}, {XKB_KEY_F27, 57390, 'u', false}, {XKB_KEY_F28, 57391, 'u', false}, {XKB_KEY_F29, 57392, 'u', false}, {XKB_KEY_F30, 57393, 'u', false}, {XKB_KEY_F31, 57394, 'u', false}, {XKB_KEY_F32, 57395, 'u', false}, {XKB_KEY_F33, 57396, 'u', false}, {XKB_KEY_F34, 57397, 'u', false}, {XKB_KEY_F35, 57398, 'u', false}, {XKB_KEY_Shift_L, 57441, 'u', true}, {XKB_KEY_Shift_R, 57447, 'u', true}, {XKB_KEY_Control_L, 57442, 'u', true}, {XKB_KEY_Control_R, 57448, 'u', true}, {XKB_KEY_Caps_Lock, 57358, 'u', true}, {XKB_KEY_Meta_L, 57446, 'u', true}, {XKB_KEY_Meta_R, 57452, 'u', true}, {XKB_KEY_Alt_L, 57443, 'u', true}, {XKB_KEY_Alt_R, 57449, 'u', true}, {XKB_KEY_Super_L, 57444, 'u', true}, {XKB_KEY_Super_R, 57450, 'u', true}, {XKB_KEY_Hyper_L, 57445, 'u', true}, {XKB_KEY_Hyper_R, 57451, 'u', true}, {XKB_KEY_Delete, 3, '~', false}, {XKB_KEY_XF86AudioLowerVolume, 57438, 'u', false}, {XKB_KEY_XF86AudioMute, 57440, 'u', false}, {XKB_KEY_XF86AudioRaiseVolume, 57439, 'u', false}, {XKB_KEY_XF86AudioPlay, 57428, 'u', false}, {XKB_KEY_XF86AudioStop, 57432, 'u', false}, {XKB_KEY_XF86AudioPrev, 57436, 'u', false}, {XKB_KEY_XF86AudioNext, 57435, 'u', false}, {XKB_KEY_XF86AudioRecord, 57437, 'u', false}, {XKB_KEY_XF86AudioPause, 57429, 'u', false}, {XKB_KEY_XF86AudioRewind, 57434, 'u', false}, {XKB_KEY_XF86AudioForward, 57433, 'u', false}, //{XKB_KEY_XF86AudioPlayPause, 57430, 'u', false}, //{XKB_KEY_XF86AudioReverse, 57431, 'u', false}, }; foot-1.21.0/log.c000066400000000000000000000134151476600145200135000ustar00rootroot00000000000000#include "log.h" #include #include #include #include #include #include #include #include #include #include "debug.h" #include "util.h" #include "xsnprintf.h" static bool colorize = false; static bool do_syslog = false; static enum log_class log_level = LOG_CLASS_NONE; static const struct { const char name[8]; const char log_prefix[7]; uint8_t color; int syslog_equivalent; } log_level_map[] = { [LOG_CLASS_NONE] = {"none", "none", 5, -1}, [LOG_CLASS_ERROR] = {"error", " err", 31, LOG_ERR}, [LOG_CLASS_WARNING] = {"warning", "warn", 33, LOG_WARNING}, [LOG_CLASS_INFO] = {"info", "info", 97, LOG_INFO}, [LOG_CLASS_DEBUG] = {"debug", " dbg", 36, LOG_DEBUG}, }; void log_init(enum log_colorize _colorize, bool _do_syslog, enum log_facility syslog_facility, enum log_class _log_level) { static const int facility_map[] = { [LOG_FACILITY_USER] = LOG_USER, [LOG_FACILITY_DAEMON] = LOG_DAEMON, }; /* Don't use colors if NO_COLOR is defined and not empty */ const char *no_color_str = getenv("NO_COLOR"); const bool no_color = no_color_str != NULL && no_color_str[0] != '\0'; colorize = _colorize == LOG_COLORIZE_ALWAYS || (_colorize == LOG_COLORIZE_AUTO && !no_color && isatty(STDERR_FILENO)); do_syslog = _do_syslog; log_level = _log_level; int slvl = log_level_map[_log_level].syslog_equivalent; if (slvl < 0) do_syslog = false; if (do_syslog) { openlog(NULL, /*LOG_PID*/0, facility_map[syslog_facility]); xassert(slvl >= 0); setlogmask(LOG_UPTO(slvl)); } } void log_deinit(void) { if (do_syslog) closelog(); } static void _log(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, int sys_errno, va_list va) { xassert(log_class > LOG_CLASS_NONE); xassert(log_class < ALEN(log_level_map)); if (log_class > log_level) return; const char *prefix = log_level_map[log_class].log_prefix; unsigned int class_clr = log_level_map[log_class].color; char clr[16]; xsnprintf(clr, sizeof(clr), "\033[%um", class_clr); fprintf(stderr, "%s%s%s: ", colorize ? clr : "", prefix, colorize ? "\033[0m" : ""); if (colorize) fputs("\033[2m", stderr); fprintf(stderr, "%s:%d: ", file, lineno); if (colorize) fputs("\033[0m", stderr); vfprintf(stderr, fmt, va); if (sys_errno != 0) fprintf(stderr, ": %s", strerror(sys_errno)); fputc('\n', stderr); } static void _sys_log(enum log_class log_class, const char *module, const char UNUSED *file, int UNUSED lineno, const char *fmt, int sys_errno, va_list va) { xassert(log_class > LOG_CLASS_NONE); xassert(log_class < ALEN(log_level_map)); if (!do_syslog) return; if (log_class > log_level) return; /* Map our log level to syslog's level */ int level = log_level_map[log_class].syslog_equivalent; char msg[4096]; int n = vsnprintf(msg, sizeof(msg), fmt, va); xassert(n >= 0); if (sys_errno != 0 && (size_t)n < sizeof(msg)) snprintf(msg + n, sizeof(msg) - n, ": %s", strerror(sys_errno)); syslog(level, "%s: %s", module, msg); } void log_msg_va(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, va_list va) { va_list va2; va_copy(va2, va); _log(log_class, module, file, lineno, fmt, 0, va); _sys_log(log_class, module, file, lineno, fmt, 0, va2); va_end(va2); } void log_msg(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) { va_list va; va_start(va, fmt); log_msg_va(log_class, module, file, lineno, fmt, va); va_end(va); } void log_errno_va(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, va_list va) { log_errno_provided_va(log_class, module, file, lineno, errno, fmt, va); } void log_errno(enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) { va_list va; va_start(va, fmt); log_errno_va(log_class, module, file, lineno, fmt, va); va_end(va); } void log_errno_provided_va(enum log_class log_class, const char *module, const char *file, int lineno, int errno_copy, const char *fmt, va_list va) { va_list va2; va_copy(va2, va); _log(log_class, module, file, lineno, fmt, errno_copy, va); _sys_log(log_class, module, file, lineno, fmt, errno_copy, va2); va_end(va2); } void log_errno_provided(enum log_class log_class, const char *module, const char *file, int lineno, int errno_copy, const char *fmt, ...) { va_list va; va_start(va, fmt); log_errno_provided_va(log_class, module, file, lineno, errno_copy, fmt, va); va_end(va); } static size_t map_len(void) { size_t len = ALEN(log_level_map); #ifndef _DEBUG /* Exclude "debug" entry for non-debug builds */ len--; #endif return len; } int log_level_from_string(const char *str) { if (unlikely(str[0] == '\0')) return -1; for (int i = 0, n = map_len(); i < n; i++) if (streq(str, log_level_map[i].name)) return i; return -1; } const char * log_level_string_hint(void) { static char buf[64]; if (buf[0] != '\0') return buf; for (size_t i = 0, pos = 0, n = map_len(); i < n; i++) { const char *entry = log_level_map[i].name; const char *delim = (i + 1 < n) ? ", " : ""; pos += xsnprintf(buf + pos, sizeof(buf) - pos, "'%s'%s", entry, delim); } return buf; } foot-1.21.0/log.h000066400000000000000000000043451476600145200135070ustar00rootroot00000000000000#pragma once #include #include #include "macros.h" enum log_colorize { LOG_COLORIZE_NEVER, LOG_COLORIZE_ALWAYS, LOG_COLORIZE_AUTO }; enum log_facility { LOG_FACILITY_USER, LOG_FACILITY_DAEMON }; enum log_class { LOG_CLASS_NONE, LOG_CLASS_ERROR, LOG_CLASS_WARNING, LOG_CLASS_INFO, LOG_CLASS_DEBUG, LOG_CLASS_COUNT, }; void log_init(enum log_colorize colorize, bool do_syslog, enum log_facility syslog_facility, enum log_class log_level); void log_deinit(void); void log_msg( enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) PRINTF(5); void log_errno( enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, ...) PRINTF(5); void log_errno_provided( enum log_class log_class, const char *module, const char *file, int lineno, int _errno, const char *fmt, ...) PRINTF(6); void log_msg_va( enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, va_list va) VPRINTF(5); void log_errno_va( enum log_class log_class, const char *module, const char *file, int lineno, const char *fmt, va_list va) VPRINTF(5); void log_errno_provided_va( enum log_class log_class, const char *module, const char *file, int lineno, int _errno, const char *fmt, va_list va) VPRINTF(6); int log_level_from_string(const char *str); const char *log_level_string_hint(void); #define LOG_ERR(...) \ log_msg(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) #define LOG_ERRNO(...) \ log_errno(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) #define LOG_ERRNO_P(_errno, ...) \ log_errno_provided(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, \ _errno, __VA_ARGS__) #define LOG_WARN(...) \ log_msg(LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) #define LOG_INFO(...) \ log_msg(LOG_CLASS_INFO, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG #define LOG_DBG(...) \ log_msg(LOG_CLASS_DEBUG, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) #else #define LOG_DBG(...) #endif foot-1.21.0/macros.h000066400000000000000000000130411476600145200142030ustar00rootroot00000000000000#pragma once #define PASTE(a, b) a##b #define XPASTE(a, b) PASTE(a, b) #define STRLEN(str) (sizeof("" str "") - 1) #define DO_PRAGMA(x) _Pragma(#x) #define VERCMP(x, y, cx, cy) ((cx > x) || ((cx == x) && (cy >= y))) #if defined(__GNUC__) && defined(__GNUC_MINOR__) #define GNUC_AT_LEAST(x, y) VERCMP(x, y, __GNUC__, __GNUC_MINOR__) #else #define GNUC_AT_LEAST(x, y) 0 #endif #if defined(__clang_major__) && defined(__clang_minor__) #define CLANG_AT_LEAST(x, y) VERCMP(x, y, __clang_major__, __clang_minor__) #else #define CLANG_AT_LEAST(x, y) 0 #endif #ifdef __has_attribute #define HAS_ATTRIBUTE(x) __has_attribute(x) #else #define HAS_ATTRIBUTE(x) 0 #endif #ifdef __has_builtin #define HAS_BUILTIN(x) __has_builtin(x) #else #define HAS_BUILTIN(x) 0 #endif #ifdef __has_include #define HAS_INCLUDE(x) __has_include(x) #else #define HAS_INCLUDE(x) 0 #endif #ifdef __has_feature #define HAS_FEATURE(x) __has_feature(x) #else #define HAS_FEATURE(x) 0 #endif // __has_extension() is a Clang macro used to determine if a feature is // available even if not standardized in the current "-std" mode. #ifdef __has_extension #define HAS_EXTENSION(x) __has_extension(x) #else // Clang versions prior to 3.0 only supported __has_feature() #define HAS_EXTENSION(x) HAS_FEATURE(x) #endif #if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(unused) || defined(__TINYC__) #define UNUSED __attribute__((__unused__)) #else #define UNUSED #endif #if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(const) #define CONST __attribute__((__const__)) #else #define CONST #endif #if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(malloc) #define MALLOC __attribute__((__malloc__)) #else #define MALLOC #endif #if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(constructor) #define CONSTRUCTOR __attribute__((__constructor__)) #define HAVE_ATTR_CONSTRUCTOR 1 #else #define CONSTRUCTOR #endif #if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(destructor) #define DESTRUCTOR __attribute__((__destructor__)) #else #define DESTRUCTOR #endif #if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(format) #define PRINTF(x) __attribute__((__format__(__printf__, (x), (x + 1)))) #define VPRINTF(x) __attribute__((__format__(__printf__, (x), 0))) #else #define PRINTF(x) #define VPRINTF(x) #endif #if (GNUC_AT_LEAST(3, 0) || HAS_BUILTIN(__builtin_expect)) && defined(__OPTIMIZE__) #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) #else #define likely(x) (x) #define unlikely(x) (x) #endif #if GNUC_AT_LEAST(3, 1) || HAS_ATTRIBUTE(noinline) #define NOINLINE __attribute__((__noinline__)) #else #define NOINLINE #endif #if GNUC_AT_LEAST(3, 1) || HAS_ATTRIBUTE(always_inline) #define ALWAYS_INLINE __attribute__((__always_inline__)) #else #define ALWAYS_INLINE #endif #if GNUC_AT_LEAST(3, 3) || HAS_ATTRIBUTE(nonnull) #define NONNULL_ARGS __attribute__((__nonnull__)) #define NONNULL_ARG(...) __attribute__((__nonnull__(__VA_ARGS__))) #else #define NONNULL_ARGS #define NONNULL_ARG(...) #endif #if GNUC_AT_LEAST(3, 4) || HAS_ATTRIBUTE(warn_unused_result) #define WARN_UNUSED_RESULT __attribute__((__warn_unused_result__)) #else #define WARN_UNUSED_RESULT #endif #if GNUC_AT_LEAST(4, 1) || HAS_ATTRIBUTE(flatten) #define FLATTEN __attribute__((__flatten__)) #else #define FLATTEN #endif #if GNUC_AT_LEAST(4, 3) || HAS_ATTRIBUTE(hot) #define HOT __attribute__((__hot__)) #else #define HOT #endif #if GNUC_AT_LEAST(4, 3) || HAS_ATTRIBUTE(cold) #define COLD __attribute__((__cold__)) #else #define COLD #endif #if GNUC_AT_LEAST(4, 5) || HAS_BUILTIN(__builtin_unreachable) #define UNREACHABLE() __builtin_unreachable() #else #define UNREACHABLE() #endif #if GNUC_AT_LEAST(5, 0) || HAS_ATTRIBUTE(returns_nonnull) #define RETURNS_NONNULL __attribute__((__returns_nonnull__)) #else #define RETURNS_NONNULL #endif #if HAS_ATTRIBUTE(diagnose_if) #define DIAGNOSE_IF(x) __attribute__((diagnose_if((x), (#x), "error"))) #else #define DIAGNOSE_IF(x) #endif #define XMALLOC MALLOC RETURNS_NONNULL WARN_UNUSED_RESULT #define XSTRDUP XMALLOC NONNULL_ARGS #if __STDC_VERSION__ >= 201112L #define noreturn _Noreturn #elif GNUC_AT_LEAST(3, 0) #define noreturn __attribute__((__noreturn__)) #else #define noreturn #endif #if CLANG_AT_LEAST(3, 6) #define UNROLL_LOOP(n) DO_PRAGMA(clang loop unroll_count(n)) #elif GNUC_AT_LEAST(8, 0) #define UNROLL_LOOP(n) DO_PRAGMA(GCC unroll (n)) #else #define UNROLL_LOOP(n) #endif #ifdef __COUNTER__ // Supported by GCC 4.3+ and Clang #define COUNTER_ __COUNTER__ #else #define COUNTER_ __LINE__ #endif #if defined(_DEBUG) && defined(HAVE_ATTR_CONSTRUCTOR) #define UNITTEST static void CONSTRUCTOR XPASTE(unittest_, COUNTER_)(void) #else #define UNITTEST static void UNUSED XPASTE(unittest_, COUNTER_)(void) #endif #ifdef __clang__ #define IGNORE_WARNING(wflag) \ DO_PRAGMA(clang diagnostic push) \ DO_PRAGMA(clang diagnostic ignored "-Wunknown-pragmas") \ DO_PRAGMA(clang diagnostic ignored "-Wunknown-warning-option") \ DO_PRAGMA(clang diagnostic ignored wflag) #define UNIGNORE_WARNINGS DO_PRAGMA(clang diagnostic pop) #elif GNUC_AT_LEAST(4, 6) #define IGNORE_WARNING(wflag) \ DO_PRAGMA(GCC diagnostic push) \ DO_PRAGMA(GCC diagnostic ignored "-Wpragmas") \ DO_PRAGMA(GCC diagnostic ignored wflag) #define UNIGNORE_WARNINGS DO_PRAGMA(GCC diagnostic pop) #else #define IGNORE_WARNING(wflag) #define UNIGNORE_WARNINGS #endif foot-1.21.0/main.c000066400000000000000000000513711476600145200136460ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "main" #define LOG_ENABLE_DBG 0 #include "log.h" #include "config.h" #include "fdm.h" #include "foot-features.h" #include "key-binding.h" #include "macros.h" #include "reaper.h" #include "render.h" #include "server.h" #include "shm.h" #include "terminal.h" #include "util.h" #include "xmalloc.h" #include "xsnprintf.h" #if !defined(__STDC_UTF_32__) || !__STDC_UTF_32__ #error "char32_t does not use UTF-32" #endif static bool fdm_sigint(struct fdm *fdm, int signo, void *data) { *(volatile sig_atomic_t *)data = true; return true; } static void print_usage(const char *prog_name) { static const char options[] = "\nOptions:\n" " -c,--config=PATH load configuration from PATH ($XDG_CONFIG_HOME/foot/foot.ini)\n" " -C,--check-config verify configuration, exit with 0 if ok, otherwise exit with 1\n" " -o,--override=[section.]key=value override configuration option\n" " -f,--font=FONT comma separated list of fonts in fontconfig format (monospace)\n" " -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n" " -T,--title=TITLE initial window title (foot)\n" " -a,--app-id=ID window application ID (foot)\n" " -m,--maximized start in maximized mode\n" " -F,--fullscreen start in fullscreen mode\n" " -L,--login-shell start shell as a login shell\n" " --pty=PATH display an existing PTY instead of creating one\n" " -D,--working-directory=DIR directory to start in (CWD)\n" " -w,--window-size-pixels=WIDTHxHEIGHT initial width and height, in pixels\n" " -W,--window-size-chars=WIDTHxHEIGHT initial width and height, in characters\n" " -s,--server[=PATH] run as a server (use 'footclient' to start terminals).\n" " Without PATH, $XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock will be used.\n" " -H,--hold remain open after child process exits\n" " -p,--print-pid=FILE|FD print PID to file or FD (only applicable in server mode)\n" " -d,--log-level={info|warning|error|none} log level (warning)\n" " -l,--log-colorize=[{never|always|auto}] enable/disable colorization of log output on stderr\n" " -S,--log-no-syslog disable syslog logging (only applicable in server mode)\n" " -v,--version show the version number and quit\n" " -e ignored (for compatibility with xterm -e)\n"; printf("Usage: %s [OPTIONS...]\n", prog_name); printf("Usage: %s [OPTIONS...] command [ARGS...]\n", prog_name); puts(options); } bool locale_is_utf8(void) { static const char u8[] = u8"ö"; xassert(strlen(u8) == 2); char32_t w; if (mbrtoc32(&w, u8, 2, &(mbstate_t){0}) != 2) return false; return w == U'ö'; } struct shutdown_context { struct terminal **term; int exit_code; }; static void term_shutdown_cb(void *data, int exit_code) { struct shutdown_context *ctx = data; *ctx->term = NULL; ctx->exit_code = exit_code; } static bool print_pid(const char *pid_file, bool *unlink_at_exit) { LOG_DBG("printing PID to %s", pid_file); errno = 0; char *end; int pid_fd = strtoul(pid_file, &end, 10); if (errno != 0 || *end != '\0') { if ((pid_fd = open(pid_file, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) < 0) { LOG_ERRNO("%s: failed to open", pid_file); return false; } else *unlink_at_exit = true; } if (pid_fd >= 0) { char pid[32]; size_t n = xsnprintf(pid, sizeof(pid), "%u\n", getpid()); ssize_t bytes = write(pid_fd, pid, n); close(pid_fd); if (bytes < 0) { LOG_ERRNO("failed to write PID to FD=%u", pid_fd); return false; } LOG_DBG("wrote %zd bytes to FD=%d", bytes, pid_fd); return true; } else return false; } static void sanitize_signals(void) { sigset_t mask; sigemptyset(&mask); sigprocmask(SIG_SETMASK, &mask, NULL); struct sigaction dfl = {.sa_handler = SIG_DFL}; sigemptyset(&dfl.sa_mask); for (int i = 1; i < SIGRTMAX; i++) sigaction(i, &dfl, NULL); } enum { PTY_OPTION = CHAR_MAX + 1, }; int main(int argc, char *const *argv) { /* Custom exit code, to enable users to differentiate between foot * itself failing, and the client application failing */ static const int foot_exit_failure = -26; int ret = foot_exit_failure; sanitize_signals(); /* XDG startup notifications */ const char *token = getenv("XDG_ACTIVATION_TOKEN"); unsetenv("XDG_ACTIVATION_TOKEN"); /* Startup notifications; we don't support it, but must ensure we * don't pass this on to programs launched by us */ unsetenv("DESKTOP_STARTUP_ID"); const char *const prog_name = argc > 0 ? argv[0] : ""; static const struct option longopts[] = { {"config", required_argument, NULL, 'c'}, {"check-config", no_argument, NULL, 'C'}, {"override", required_argument, NULL, 'o'}, {"term", required_argument, NULL, 't'}, {"title", required_argument, NULL, 'T'}, {"app-id", required_argument, NULL, 'a'}, {"login-shell", no_argument, NULL, 'L'}, {"working-directory", required_argument, NULL, 'D'}, {"font", required_argument, NULL, 'f'}, {"window-size-pixels", required_argument, NULL, 'w'}, {"window-size-chars", required_argument, NULL, 'W'}, {"server", optional_argument, NULL, 's'}, {"hold", no_argument, NULL, 'H'}, {"maximized", no_argument, NULL, 'm'}, {"fullscreen", no_argument, NULL, 'F'}, {"presentation-timings", no_argument, NULL, 'P'}, /* Undocumented */ {"pty", required_argument, NULL, PTY_OPTION}, {"print-pid", required_argument, NULL, 'p'}, {"log-level", required_argument, NULL, 'd'}, {"log-colorize", optional_argument, NULL, 'l'}, {"log-no-syslog", no_argument, NULL, 'S'}, {"version", no_argument, NULL, 'v'}, {"help", no_argument, NULL, 'h'}, {NULL, no_argument, NULL, 0}, }; bool check_config = false; const char *conf_path = NULL; const char *custom_cwd = NULL; const char *pty_path = NULL; bool as_server = false; const char *conf_server_socket_path = NULL; bool presentation_timings = false; bool hold = false; bool unlink_pid_file = false; const char *pid_file = NULL; enum log_class log_level = LOG_CLASS_WARNING; enum log_colorize log_colorize = LOG_COLORIZE_AUTO; bool log_syslog = true; user_notifications_t user_notifications = tll_init(); config_override_t overrides = tll_init(); while (true) { int c = getopt_long(argc, argv, "+c:Co:t:T:a:LD:f:w:W:s::HmFPp:d:l::Sveh", longopts, NULL); if (c == -1) break; switch (c) { case 'c': conf_path = optarg; break; case 'C': check_config = true; break; case 'o': tll_push_back(overrides, xstrdup(optarg)); break; case 't': tll_push_back(overrides, xstrjoin("term=", optarg)); break; case 'L': tll_push_back(overrides, xstrdup("login-shell=yes")); break; case 'T': tll_push_back(overrides, xstrjoin("title=", optarg)); break; case 'a': tll_push_back(overrides, xstrjoin("app-id=", optarg)); break; case 'D': { struct stat st; if (stat(optarg, &st) < 0 || !(st.st_mode & S_IFDIR)) { fprintf(stderr, "error: %s: not a directory\n", optarg); return ret; } custom_cwd = optarg; break; } case 'f': { char *font_override = xstrjoin("font=", optarg); tll_push_back(overrides, font_override); break; } case 'w': { unsigned width, height; if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) { fprintf(stderr, "error: invalid window-size-pixels: %s\n", optarg); return ret; } tll_push_back( overrides, xasprintf("initial-window-size-pixels=%ux%u", width, height)); break; } case 'W': { unsigned width, height; if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) { fprintf(stderr, "error: invalid window-size-chars: %s\n", optarg); return ret; } tll_push_back( overrides, xasprintf("initial-window-size-chars=%ux%u", width, height)); break; } case 's': as_server = true; if (optarg != NULL) conf_server_socket_path = optarg; break; case PTY_OPTION: pty_path = optarg; break; case 'P': presentation_timings = true; break; case 'H': hold = true; break; case 'm': tll_push_back(overrides, xstrdup("initial-window-mode=maximized")); break; case 'F': tll_push_back(overrides, xstrdup("initial-window-mode=fullscreen")); break; case 'p': pid_file = optarg; break; case 'd': { int lvl = log_level_from_string(optarg); if (unlikely(lvl < 0)) { fprintf( stderr, "-d,--log-level: %s: argument must be one of %s\n", optarg, log_level_string_hint()); return ret; } log_level = lvl; break; } case 'l': if (optarg == NULL || streq(optarg, "auto")) log_colorize = LOG_COLORIZE_AUTO; else if (streq(optarg, "never")) log_colorize = LOG_COLORIZE_NEVER; else if (streq(optarg, "always")) log_colorize = LOG_COLORIZE_ALWAYS; else { fprintf(stderr, "%s: argument must be one of 'never', 'always' or 'auto'\n", optarg); return ret; } break; case 'S': log_syslog = false; break; case 'v': print_version_and_features("foot "); return EXIT_SUCCESS; case 'h': print_usage(prog_name); return EXIT_SUCCESS; case 'e': break; case '?': return ret; } } if (as_server && pty_path) { fputs("error: --pty is incompatible with server mode\n", stderr); return ret; } log_init(log_colorize, as_server && log_syslog, as_server ? LOG_FACILITY_DAEMON : LOG_FACILITY_USER, log_level); if (argc > 0) { argc -= optind; argv += optind; } LOG_INFO("%s", version_and_features); { struct utsname name; if (uname(&name) < 0) LOG_ERRNO("uname() failed"); else LOG_INFO("arch: %s %s/%zu-bit", name.sysname, name.machine, sizeof(void *) * 8); } srand(time(NULL)); const char *locale = setlocale(LC_CTYPE, ""); if (locale == NULL) { /* * If the user has configured an invalid locale, or a name of a locale * that does not exist on this system, then the above call may return * NULL. We should just continue with the fallback method below. */ LOG_ERR("setlocale() failed. The most common cause is that the " "configured locale is not available, or has been misspelled"); } LOG_INFO("locale: %s", locale != NULL ? locale : ""); bool bad_locale = locale == NULL || !locale_is_utf8(); if (bad_locale) { static const char fallback_locales[][12] = { "C.UTF-8", "en_US.UTF-8", }; char *saved_locale = locale != NULL ? xstrdup(locale) : NULL; /* * Try to force an UTF-8 locale. If we succeed, launch the * user's shell as usual, but add a user-notification saying * the locale has been changed. */ for (size_t i = 0; i < ALEN(fallback_locales); i++) { const char *const fallback_locale = fallback_locales[i]; if (setlocale(LC_CTYPE, fallback_locale) != NULL) { if (saved_locale != NULL) { LOG_WARN( "'%s' is not a UTF-8 locale, falling back to '%s'", saved_locale, fallback_locale); user_notification_add_fmt( &user_notifications, USER_NOTIFICATION_WARNING, "'%s' is not a UTF-8 locale, falling back to '%s'", saved_locale, fallback_locale); } else { LOG_WARN( "invalid locale, falling back to '%s'", fallback_locale); user_notification_add_fmt( &user_notifications, USER_NOTIFICATION_WARNING, "invalid locale, falling back to '%s'", fallback_locale); } bad_locale = false; break; } } if (bad_locale) { if (saved_locale != NULL) { LOG_ERR( "'%s' is not a UTF-8 locale, and failed to find a fallback", saved_locale); user_notification_add_fmt( &user_notifications, USER_NOTIFICATION_ERROR, "'%s' is not a UTF-8 locale, and failed to find a fallback", saved_locale); } else { LOG_ERR("invalid locale, and failed to find a fallback"); user_notification_add_fmt( &user_notifications, USER_NOTIFICATION_ERROR, "invalid locale, and failed to find a fallback"); } } free(saved_locale); } struct config conf = {NULL}; bool conf_successful = config_load( &conf, conf_path, &user_notifications, &overrides, check_config, as_server); tll_free_and_free(overrides, free); if (!conf_successful) { config_free(&conf); return ret; } if (check_config) { config_free(&conf); return EXIT_SUCCESS; } _Static_assert((int)LOG_CLASS_ERROR == (int)FCFT_LOG_CLASS_ERROR, "fcft log level enum offset"); _Static_assert((int)LOG_COLORIZE_ALWAYS == (int)FCFT_LOG_COLORIZE_ALWAYS, "fcft colorize enum mismatch"); fcft_init( (enum fcft_log_colorize)log_colorize, as_server && log_syslog, (enum fcft_log_class)log_level); if (conf_server_socket_path != NULL) { free(conf.server_socket_path); conf.server_socket_path = xstrdup(conf_server_socket_path); } conf.presentation_timings = presentation_timings; conf.hold_at_exit = hold; if (conf.tweak.font_monospace_warn && conf.fonts[0].count > 0) { check_if_font_is_monospaced( conf.fonts[0].arr[0].pattern, &conf.notifications); } if (bad_locale) { static char *const bad_locale_fake_argv[] = {"/bin/sh", "-c", "", NULL}; argc = 1; argv = bad_locale_fake_argv; conf.hold_at_exit = true; } struct fdm *fdm = NULL; struct reaper *reaper = NULL; struct key_binding_manager *key_binding_manager = NULL; struct wayland *wayl = NULL; struct renderer *renderer = NULL; struct terminal *term = NULL; struct server *server = NULL; struct shutdown_context shutdown_ctx = {.term = &term, .exit_code = foot_exit_failure}; const char *cwd = custom_cwd; char *_cwd = NULL; if (cwd == NULL) { size_t buf_len = 1024; do { _cwd = xrealloc(_cwd, buf_len); errno = 0; if (getcwd(_cwd, buf_len) == NULL && errno != ERANGE) { LOG_ERRNO("failed to get current working directory"); goto out; } buf_len *= 2; } while (errno == ERANGE); cwd = _cwd; } const char *pwd = getenv("PWD"); if (pwd != NULL) { char *resolved_path_cwd = realpath(cwd, NULL); char *resolved_path_pwd = realpath(pwd, NULL); if (resolved_path_cwd != NULL && resolved_path_pwd != NULL && streq(resolved_path_cwd, resolved_path_pwd)) { /* * The resolved path of $PWD matches the resolved path of * the *actual* working directory - use $PWD. * * This makes a difference when $PWD refers to a symlink. */ cwd = pwd; } free(resolved_path_cwd); free(resolved_path_pwd); } shm_set_max_pool_size(conf.tweak.max_shm_pool_size); if ((fdm = fdm_init()) == NULL) goto out; if ((reaper = reaper_init(fdm)) == NULL) goto out; if ((key_binding_manager = key_binding_manager_new()) == NULL) goto out; if ((wayl = wayl_init( fdm, key_binding_manager, conf.presentation_timings)) == NULL) { goto out; } if ((renderer = render_init(fdm, wayl)) == NULL) goto out; if (!as_server && (term = term_init( &conf, fdm, reaper, wayl, "foot", cwd, token, pty_path, argc, argv, NULL, &term_shutdown_cb, &shutdown_ctx)) == NULL) { goto out; } free(_cwd); _cwd = NULL; if (as_server && (server = server_init(&conf, fdm, reaper, wayl)) == NULL) goto out; volatile sig_atomic_t aborted = false; if (!fdm_signal_add(fdm, SIGINT, &fdm_sigint, (void *)&aborted) || !fdm_signal_add(fdm, SIGTERM, &fdm_sigint, (void *)&aborted)) { goto out; } struct sigaction sig_ign = {.sa_handler = SIG_IGN}; sigemptyset(&sig_ign.sa_mask); if (sigaction(SIGHUP, &sig_ign, NULL) < 0 || sigaction(SIGPIPE, &sig_ign, NULL) < 0) { LOG_ERRNO("failed to ignore SIGHUP+SIGPIPE"); goto out; } if (as_server) LOG_INFO("running as server; launch terminals by running footclient"); if (as_server && pid_file != NULL) { if (!print_pid(pid_file, &unlink_pid_file)) goto out; } ret = EXIT_SUCCESS; while (likely(!aborted && (as_server || tll_length(wayl->terms) > 0))) { if (unlikely(!fdm_poll(fdm))) { ret = foot_exit_failure; break; } } out: free(_cwd); server_destroy(server); term_destroy(term); shm_fini(); render_destroy(renderer); wayl_destroy(wayl); key_binding_manager_destroy(key_binding_manager); reaper_destroy(reaper); fdm_signal_del(fdm, SIGTERM); fdm_signal_del(fdm, SIGINT); fdm_destroy(fdm); config_free(&conf); if (unlink_pid_file) unlink(pid_file); LOG_INFO("goodbye"); fcft_fini(); log_deinit(); return ret == EXIT_SUCCESS && !as_server ? shutdown_ctx.exit_code : ret; } UNITTEST { char *s = xstrjoin("foo", "bar"); xassert(streq(s, "foobar")); free(s); s = xstrjoin3("foo", " ", "bar"); xassert(streq(s, "foo bar")); free(s); s = xstrjoin3("foo", ",", "bar"); xassert(streq(s, "foo,bar")); free(s); s = xstrjoin3("foo", "bar", "baz"); xassert(streq(s, "foobarbaz")); free(s); } foot-1.21.0/meson.build000066400000000000000000000313621476600145200147160ustar00rootroot00000000000000project('foot', 'c', version: '1.21.0', license: 'MIT', meson_version: '>=0.59.0', default_options: [ 'c_std=c11', 'warning_level=1', 'werror=true', 'b_ndebug=if-release']) is_debug_build = get_option('buildtype').startswith('debug') cc = meson.get_compiler('c') if cc.has_function('memfd_create', args: ['-D_GNU_SOURCE'], prefix: '#include ') add_project_arguments('-DMEMFD_CREATE', language: 'c') endif # Missing on DragonFly, FreeBSD < 14.1 if cc.has_function('execvpe', args: ['-D_GNU_SOURCE'], prefix: '#include ') add_project_arguments('-DEXECVPE', language: 'c') endif utmp_backend = get_option('utmp-backend') if utmp_backend == 'auto' host_os = host_machine.system() if host_os == 'linux' utmp_backend = 'libutempter' elif host_os == 'freebsd' utmp_backend = 'ulog' else utmp_backend = 'none' endif endif utmp_default_helper_path = get_option('utmp-default-helper-path') if utmp_backend == 'none' utmp_add = '' utmp_del = '' utmp_del_have_argument = false utmp_default_helper_path = '' elif utmp_backend == 'libutempter' utmp_add = 'add' utmp_del = 'del' utmp_del_have_argument = true if utmp_default_helper_path == 'auto' utmp_default_helper_path = join_paths('/usr', get_option('libdir'), 'utempter', 'utempter') endif elif utmp_backend == 'ulog' utmp_add = 'login' utmp_del = 'logout' utmp_del_have_argument = false if utmp_default_helper_path == 'auto' utmp_default_helper_path = join_paths('/usr', get_option('libexecdir'), 'ulog-helper') endif else error('invalid utmp backend') endif add_project_arguments( ['-D_GNU_SOURCE=200809L', '-DFOOT_DEFAULT_TERM="@0@"'.format(get_option('default-terminfo'))] + (utmp_backend != 'none' ? ['-DUTMP_ADD="@0@"'.format(utmp_add), '-DUTMP_DEL="@0@"'.format(utmp_del), '-DUTMP_DEFAULT_HELPER_PATH="@0@"'.format(utmp_default_helper_path)] : []) + (utmp_del_have_argument ? ['-DUTMP_DEL_HAVE_ARGUMENT=1'] : []) + (is_debug_build ? ['-D_DEBUG'] : [cc.get_supported_arguments('-fno-asynchronous-unwind-tables')]) + (get_option('ime') ? ['-DFOOT_IME_ENABLED=1'] : []) + (get_option('b_pgo') == 'use' ? ['-DFOOT_PGO_ENABLED=1'] : []) + cc.get_supported_arguments( ['-pedantic', '-fstrict-aliasing', '-Wstrict-aliasing']), language: 'c', ) terminfo_install_location = get_option('custom-terminfo-install-location') if terminfo_install_location != '' add_project_arguments( ['-DFOOT_TERMINFO_PATH="@0@"'.format( join_paths(get_option('prefix'), terminfo_install_location))], language: 'c') else terminfo_install_location = join_paths(get_option('datadir'), 'terminfo') endif # Compute the relative path used by compiler invocations. source_root = meson.current_source_dir().split('/') build_root = meson.global_build_root().split('/') relative_dir_parts = [] i = 0 in_prefix = true foreach p : build_root if i >= source_root.length() or not in_prefix or p != source_root[i] in_prefix = false relative_dir_parts += '..' endif i += 1 endforeach i = 0 in_prefix = true foreach p : source_root if i >= build_root.length() or not in_prefix or build_root[i] != p in_prefix = false relative_dir_parts += p endif i += 1 endforeach relative_dir = join_paths(relative_dir_parts) + '/' if cc.has_argument('-fmacro-prefix-map=/foo=') add_project_arguments('-fmacro-prefix-map=@0@='.format(relative_dir), language: 'c') endif math = cc.find_library('m') threads = [dependency('threads'), cc.find_library('stdthreads', required: false)] libepoll = dependency('epoll-shim', required: false) pixman = dependency('pixman-1') wayland_protocols = dependency('wayland-protocols', version: '>=1.41', fallback: 'wayland-protocols', default_options: ['tests=false']) wayland_client = dependency('wayland-client') wayland_cursor = dependency('wayland-cursor') xkb = dependency('xkbcommon', version: '>=1.0.0') fontconfig = dependency('fontconfig') utf8proc = dependency('libutf8proc', required: get_option('grapheme-clustering')) if utf8proc.found() add_project_arguments('-DFOOT_GRAPHEME_CLUSTERING=1', language: 'c') endif tllist = dependency('tllist', version: '>=1.1.0', fallback: 'tllist') fcft = dependency('fcft', version: ['>=3.3.1', '<4.0.0'], fallback: 'fcft') wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir') wscanner = dependency('wayland-scanner', native: true) wscanner_prog = find_program( wscanner.get_variable('wayland_scanner'), native: true) wl_proto_headers = [] wl_proto_src = [] wl_proto_xml = [ wayland_protocols_datadir / 'stable/xdg-shell/xdg-shell.xml', wayland_protocols_datadir / 'unstable/xdg-decoration/xdg-decoration-unstable-v1.xml', wayland_protocols_datadir / 'unstable/xdg-output/xdg-output-unstable-v1.xml', wayland_protocols_datadir / 'unstable/primary-selection/primary-selection-unstable-v1.xml', wayland_protocols_datadir / 'stable/presentation-time/presentation-time.xml', wayland_protocols_datadir / 'unstable/text-input/text-input-unstable-v3.xml', wayland_protocols_datadir / 'staging/xdg-activation/xdg-activation-v1.xml', wayland_protocols_datadir / 'stable/viewporter/viewporter.xml', wayland_protocols_datadir / 'staging/fractional-scale/fractional-scale-v1.xml', wayland_protocols_datadir / 'unstable/tablet/tablet-unstable-v2.xml', # required by cursor-shape-v1 wayland_protocols_datadir / 'staging/cursor-shape/cursor-shape-v1.xml', wayland_protocols_datadir / 'staging/single-pixel-buffer/single-pixel-buffer-v1.xml', wayland_protocols_datadir / 'staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml', wayland_protocols_datadir / 'staging/xdg-system-bell/xdg-system-bell-v1.xml', wayland_protocols_datadir / 'staging/color-management/color-management-v1.xml', ] foreach prot : wl_proto_xml wl_proto_headers += custom_target( prot.underscorify() + '-client-header', output: '@BASENAME@.h', input: prot, command: [wscanner_prog, 'client-header', '@INPUT@', '@OUTPUT@']) wl_proto_src += custom_target( prot.underscorify() + '-private-code', output: '@BASENAME@.c', input: prot, command: [wscanner_prog, 'private-code', '@INPUT@', '@OUTPUT@']) endforeach env = find_program('env', native: true) generate_version_sh = files('generate-version.sh') version = custom_target( 'generate_version', build_always_stale: true, output: 'version.h', command: [env, 'LC_ALL=C', generate_version_sh, meson.project_version(), '@CURRENT_SOURCE_DIR@', '@OUTPUT@']) python = find_program('python3', native: true) generate_builtin_terminfo_py = files('scripts/generate-builtin-terminfo.py') foot_terminfo = files('foot.info') builtin_terminfo = custom_target( 'generate_builtin_terminfo', output: 'foot-terminfo.h', command: [python, generate_builtin_terminfo_py, '@default_terminfo@', foot_terminfo, 'foot', '@OUTPUT@'] ) generate_emoji_variation_sequences = files('scripts/generate-emoji-variation-sequences.py') emoji_variation_sequences = custom_target( 'generate_emoji_variation_sequences', input: 'unicode/emoji-variation-sequences.txt', output: 'emoji-variation-sequences.h', command: [python, generate_emoji_variation_sequences, '@INPUT@', '@OUTPUT@'] ) generate_srgb_funcs = files('scripts/srgb.py') srgb_funcs = custom_target( 'generate_srgb_funcs', output: ['srgb.c', 'srgb.h'], command: [python, generate_srgb_funcs, '@OUTPUT0@', '@OUTPUT1@'] ) common = static_library( 'common', 'log.c', 'log.h', 'char32.c', 'char32.h', 'debug.c', 'debug.h', 'macros.h', 'xmalloc.c', 'xmalloc.h', 'xsnprintf.c', 'xsnprintf.h', dependencies: [utf8proc] ) misc = static_library( 'misc', 'hsl.c', 'hsl.h', 'macros.h', 'misc.c', 'misc.h', 'uri.c', 'uri.h', dependencies: [utf8proc], link_with: [common] ) vtlib = static_library( 'vtlib', 'base64.c', 'base64.h', 'composed.c', 'composed.h', 'cursor-shape.c', 'cursor-shape.h', 'csi.c', 'csi.h', 'dcs.c', 'dcs.h', 'macros.h', 'osc.c', 'osc.h', 'sixel.c', 'sixel.h', 'vt.c', 'vt.h', builtin_terminfo, emoji_variation_sequences, srgb_funcs, wl_proto_src + wl_proto_headers, version, dependencies: [libepoll, pixman, fcft, tllist, wayland_client, xkb, utf8proc], link_with: [common, misc], ) pgolib = static_library( 'pgolib', 'grid.c', 'grid.h', 'selection.c', 'selection.h', 'terminal.c', 'terminal.h', wl_proto_src + wl_proto_headers, dependencies: [libepoll, pixman, fcft, tllist, wayland_client, xkb, utf8proc], link_with: vtlib, ) tokenize = static_library( 'tokenizelib', 'tokenize.c', dependencies: [utf8proc], link_with: [common], ) if get_option('b_pgo') == 'generate' executable( 'pgo', 'pgo/pgo.c', wl_proto_src + wl_proto_headers, dependencies: [math, threads, libepoll, pixman, wayland_client, xkb, utf8proc, fcft, tllist], link_with: pgolib, ) endif executable( 'foot', 'async.c', 'async.h', 'box-drawing.c', 'box-drawing.h', 'config.c', 'config.h', 'commands.c', 'commands.h', 'extract.c', 'extract.h', 'fdm.c', 'fdm.h', 'foot-features.c', 'foot-features.h', 'ime.c', 'ime.h', 'input.c', 'input.h', 'key-binding.c', 'key-binding.h', 'main.c', 'notify.c', 'notify.h', 'quirks.c', 'quirks.h', 'reaper.c', 'reaper.h', 'render.c', 'render.h', 'search.c', 'search.h', 'server.c', 'server.h', 'client-protocol.h', 'shm.c', 'shm.h', 'slave.c', 'slave.h', 'spawn.c', 'spawn.h', 'tokenize.c', 'tokenize.h', 'unicode-mode.c', 'unicode-mode.h', 'url-mode.c', 'url-mode.h', 'user-notification.c', 'user-notification.h', 'wayland.c', 'wayland.h', 'shm-formats.h', wl_proto_src + wl_proto_headers, version, dependencies: [math, threads, libepoll, pixman, wayland_client, wayland_cursor, xkb, fontconfig, utf8proc, tllist, fcft], link_with: pgolib, install: true) executable( 'footclient', 'client.c', 'client-protocol.h', 'foot-features.c', 'foot-features.h', 'macros.h', 'util.h', version, dependencies: [tllist, utf8proc], link_with: common, install: true) install_data( 'foot.desktop', 'foot-server.desktop', 'footclient.desktop', install_dir: join_paths(get_option('datadir'), 'applications')) systemd = dependency('systemd', required: false) custom_systemd_units_dir = get_option('systemd-units-dir') if systemd.found() or custom_systemd_units_dir != '' configuration = configuration_data() configuration.set('bindir', join_paths(get_option('prefix'), get_option('bindir'))) if (custom_systemd_units_dir == '') systemd_units_dir = systemd.get_variable('systemduserunitdir') else systemd_units_dir = custom_systemd_units_dir endif configure_file( configuration: configuration, input: 'foot-server.service.in', output: '@BASENAME@', install_dir: systemd_units_dir ) install_data( 'foot-server.socket', install_dir: systemd_units_dir) endif scdoc = dependency('scdoc', native: true, required: get_option('docs')) install_data('foot.ini', install_dir: join_paths(get_option('sysconfdir'), 'xdg', 'foot')) if scdoc.found() install_data( 'LICENSE', 'README.md', 'CHANGELOG.md', install_dir: join_paths(get_option('datadir'), 'doc', 'foot')) subdir('doc') endif if get_option('themes') install_subdir('themes', install_dir: join_paths(get_option('datadir'), 'foot')) endif terminfo_base_name = get_option('terminfo-base-name') if terminfo_base_name == '' terminfo_base_name = get_option('default-terminfo') endif tic = find_program('tic', native: true, required: get_option('terminfo')) if tic.found() conf_data = configuration_data( { 'default_terminfo': terminfo_base_name } ) preprocessed = configure_file( input: 'foot.info', output: 'foot.info.preprocessed', configuration: conf_data, ) custom_target( 'terminfo', output: terminfo_base_name[0], input: preprocessed, command: [tic, '-x', '-o', '@OUTDIR@', '-e', '@0@,@0@-direct'.format(terminfo_base_name), '@INPUT@'], install: true, install_dir: terminfo_install_location ) endif subdir('completions') subdir('icons') subdir('utils') if (get_option('tests')) subdir('tests') endif summary( { 'Documentation': scdoc.found(), 'Themes': get_option('themes'), 'IME': get_option('ime'), 'Grapheme clustering': utf8proc.found(), 'utmp backend': utmp_backend, 'utmp helper default path': utmp_default_helper_path, 'Build terminfo': tic.found(), 'Terminfo base name': terminfo_base_name, 'Terminfo install location': terminfo_install_location, 'Default TERM': get_option('default-terminfo'), 'Set TERMINFO': get_option('custom-terminfo-install-location') != '', 'Build tests': get_option('tests'), }, bool_yn: true ) foot-1.21.0/meson_options.txt000066400000000000000000000035341476600145200162110ustar00rootroot00000000000000option('docs', type: 'feature', description: 'Build and install documentation (man pages, example foot.ini, readme, changelog, license etc).') option('themes', type: 'boolean', value: true, description: 'Install themes (predefined color schemes)') option('ime', type: 'boolean', value: true, description: 'IME (Input Method Editor) support') option('grapheme-clustering', type: 'feature', description: 'Enables grapheme clustering using libutf8proc. Requires fcft with harfbuzz support to be useful.') option('tests', type: 'boolean', value: true, description: 'Build tests') option('terminfo', type: 'feature', value: 'enabled', description: 'Build and install foot\'s terminfo files.') option('default-terminfo', type: 'string', value: 'foot', description: 'Default value of the "term" option in foot.ini.') option('terminfo-base-name', type: 'string', description: 'Base name of the generated terminfo files. Defaults to the value of the \'default-terminfo\' meson option') option('custom-terminfo-install-location', type: 'string', value: '', description: 'Path to foot\'s terminfo, relative to ${prefix}. If set, foot will set $TERMINFO to this value in the client process.') option('systemd-units-dir', type: 'string', value: '', description: 'Where to install the systemd service files (absolute path). Default: ${systemduserunitdir}') option('utmp-backend', type: 'combo', value: 'auto', choices: ['none', 'libutempter', 'ulog', 'auto'], description: 'Which utmp logging backend to use. This affects how (with what arguments) the utmp helper binary (see \'utmp-default-helper-path\')is called. Default: auto (linux=libutempter, freebsd=ulog, others=none)') option('utmp-default-helper-path', type: 'string', value: 'auto', description: 'Default path to the utmp helper binary. Default: auto-detect') foot-1.21.0/misc.c000066400000000000000000000025131476600145200136470ustar00rootroot00000000000000#include "misc.h" #include "char32.h" #include bool isword(char32_t wc, bool spaces_only, const char32_t *delimiters) { if (spaces_only) return isc32graph(wc); if (c32chr(delimiters, wc) != NULL) return false; return isc32graph(wc); } void timespec_add(const struct timespec *a, const struct timespec *b, struct timespec *res) { const long one_sec_in_ns = 1000000000; res->tv_sec = a->tv_sec + b->tv_sec; res->tv_nsec = a->tv_nsec + b->tv_nsec; /* tv_nsec may be negative */ if (res->tv_nsec >= one_sec_in_ns) { res->tv_sec++; res->tv_nsec -= one_sec_in_ns; } } void timespec_sub(const struct timespec *a, const struct timespec *b, struct timespec *res) { const long one_sec_in_ns = 1000000000; res->tv_sec = a->tv_sec - b->tv_sec; res->tv_nsec = a->tv_nsec - b->tv_nsec; /* tv_nsec may be negative */ if (res->tv_nsec < 0) { res->tv_sec--; res->tv_nsec += one_sec_in_ns; } } bool is_valid_utf8_and_printable(const char *value) { char32_t *wide = ambstoc32(value); if (wide == NULL) return false; for (const char32_t *c = wide; *c != U'\0'; c++) { if (!isc32print(*c)) { free(wide); return false; } } free(wide); return true; } foot-1.21.0/misc.h000066400000000000000000000006021476600145200136510ustar00rootroot00000000000000#pragma once #include #include #include bool isword(char32_t wc, bool spaces_only, const char32_t *delimiters); void timespec_add(const struct timespec *a, const struct timespec *b, struct timespec *res); void timespec_sub(const struct timespec *a, const struct timespec *b, struct timespec *res); bool is_valid_utf8_and_printable(const char *value); foot-1.21.0/notify.c000066400000000000000000000564121476600145200142330ustar00rootroot00000000000000#include "notify.h" #include #include #include #include #include #include #include #define LOG_MODULE "notify" #define LOG_ENABLE_DBG 0 #include "log.h" #include "config.h" #include "spawn.h" #include "terminal.h" #include "util.h" #include "wayland.h" #include "xmalloc.h" #include "xsnprintf.h" void notify_free(struct terminal *term, struct notification *notif) { if (notif->pid > 0) fdm_del(term->fdm, notif->stdout_fd); free(notif->id); free(notif->title); free(notif->body); free(notif->category); free(notif->app_id); free(notif->icon_cache_id); free(notif->icon_symbolic_name); free(notif->icon_data); free(notif->sound_name); free(notif->xdg_token); free(notif->stdout_data); tll_free_and_free(notif->actions, free); if (notif->icon_path != NULL) { unlink(notif->icon_path); free(notif->icon_path); if (notif->icon_fd >= 0) close(notif->icon_fd); } memset(notif, 0, sizeof(*notif)); } static bool write_icon_file(const void *data, size_t data_sz, int *fd, char **filename, char **symbolic_name) { xassert(*filename == NULL); xassert(*symbolic_name == NULL); char name[64] = "/tmp/foot-notification-icon-XXXXXX"; *filename = NULL; *symbolic_name = NULL; *fd = mkostemp(name, O_CLOEXEC); if (*fd < 0) { LOG_ERRNO("failed to create temporary file for icon cache"); return false; } if (write(*fd, data, data_sz) != (ssize_t)data_sz) { LOG_ERRNO("failed to write icon data to temporary file"); close(*fd); *fd = -1; return false; } LOG_DBG("wrote icon data to %s", name); *filename = xstrdup(name); *symbolic_name = xstrjoin("file://", *filename); return true; } static bool to_integer(const char *line, size_t len, uint32_t *res) { bool is_id = true; uint32_t maybe_id = 0; for (size_t i = 0; i < len; i++) { char digit = line[i]; if (digit < '0' || digit > '9') { is_id = false; break; } maybe_id *= 10; maybe_id += digit - '0'; } *res = maybe_id; return is_id; } static void consume_stdout(struct notification *notif, bool eof) { char *data = notif->stdout_data; const char *line = data; size_t left = notif->stdout_sz; /* Process stdout, line-by-line */ while (left > 0) { line = data; size_t len = left; char *eol = memchr(line, '\n', left); if (eol != NULL) { *eol = '\0'; len = strlen(line); data = eol + 1; } else if (!eof) break; uint32_t maybe_id = 0; uint32_t maybe_button_nr = 0; /* Check for daemon assigned ID, either '123', or 'id=123' */ if ((notif->external_id == 0 && to_integer(line, len, &maybe_id)) || (len > 3 && memcmp(line, "id=", 3) == 0 && to_integer(&line[3], len - 3, &maybe_id))) { notif->external_id = maybe_id; LOG_DBG("external ID: %u", notif->external_id); } /* Check for triggered action, either 'default' or 'action=default' */ else if ((len == 7 && memcmp(line, "default", 7) == 0) || (len == 7 + 7 && memcmp(line, "action=default", 7 + 7) == 0)) { notif->activated = true; LOG_DBG("notification's default action was triggered"); } else if (len > 7 && memcmp(line, "action=", 7) == 0) { notif->activated = true; if (to_integer(&line[7], len - 7, &maybe_button_nr)) { notif->activated_button = maybe_button_nr; LOG_DBG("custom action %u triggered", notif->activated_button); } else { LOG_DBG("unrecognized action triggered: %.*s", (int)(len - 7), &line[7]); } } else if (notif->external_id > 0 && to_integer(line, len, &maybe_button_nr) && maybe_button_nr > 0 && maybe_button_nr <= notif->button_count) { /* Single integer, appearing *after* the ID, and is within the custom button/action range */ notif->activated = true; notif->activated_button = maybe_button_nr; LOG_DBG("custom action %u triggered", notif->activated_button); } /* Check for XDG activation token, 'xdgtoken=xyz' */ else if (len > 9 && memcmp(line, "xdgtoken=", 9) == 0) { notif->xdg_token = xstrndup(&line[9], len - 9); LOG_DBG("XDG token: \"%s\"", notif->xdg_token); } left -= len + (eol != NULL ? 1 : 0); } if (left > 0) memmove(notif->stdout_data, data, left); notif->stdout_sz = left; } static bool fdm_notify_stdout(struct fdm *fdm, int fd, int events, void *data) { const struct terminal *term = data; struct notification *notif = NULL; /* Find notification */ tll_foreach(term->active_notifications, it) { if (it->item.stdout_fd == fd) { notif = &it->item; break; } } if (events & EPOLLIN) { char buf[512]; ssize_t count = read(fd, buf, sizeof(buf) - 1); if (count < 0) { if (errno == EINTR) return true; LOG_ERRNO("failed to read notification activation token"); return false; } if (count > 0 && notif != NULL) { if (notif->stdout_data == NULL) { xassert(notif->stdout_sz == 0); notif->stdout_data = xmemdup(buf, count); } else { notif->stdout_data = xrealloc(notif->stdout_data, notif->stdout_sz + count); memcpy(¬if->stdout_data[notif->stdout_sz], buf, count); } notif->stdout_sz += count; consume_stdout(notif, false); } } if (events & EPOLLHUP) { fdm_del(fdm, fd); if (notif != NULL) { notif->stdout_fd = -1; consume_stdout(notif, true); } } return true; } static void notif_done(struct reaper *reaper, pid_t pid, int status, void *data) { struct terminal *term = data; tll_foreach(term->active_notifications, it) { struct notification *notif = &it->item; if (notif->pid != pid) continue; LOG_DBG("notification %s closed", notif->id != NULL ? notif->id : ""); if (notif->activated && notif->focus) { LOG_DBG("focus window on notification activation: \"%s\"", notif->xdg_token); if (notif->xdg_token == NULL) LOG_WARN("cannot focus window: no activation token available"); else wayl_activate(term->wl, term->window, notif->xdg_token); } if (notif->activated && notif->report_activated) { LOG_DBG("sending notification activation event to client"); const char *id = notif->id != NULL ? notif->id : "0"; char button_nr[16] = {0}; if (notif->activated_button > 0) { xsnprintf( button_nr, sizeof(button_nr), "%u", notif->activated_button); } char reply[7 + strlen(id) + 1 + strlen(button_nr) + 2 + 1]; size_t n = xsnprintf( reply, sizeof(reply), "\033]99;i=%s;%s\033\\", id, button_nr); term_to_slave(term, reply, n); } if (notif->report_closed) { LOG_DBG("sending notification close event to client"); const char *id = notif->id != NULL ? notif->id : "0"; char reply[7 + strlen(id) + 1 + 7 + 1 + 2 + 1]; size_t n = xsnprintf( reply, sizeof(reply), "\033]99;i=%s:p=close;\033\\", id); term_to_slave(term, reply, n); } notify_free(term, notif); tll_remove(term->active_notifications, it); return; } } static bool expand_action_to_argv(struct terminal *term, const char *name, const char *label, size_t *argc, char ***argv) { char **expanded = NULL; size_t count = 0; if (!spawn_expand_template( &term->conf->desktop_notifications.command_action_arg, 2, (const char *[]){"action-name", "action-label"}, (const char *[]){name, label}, &count, &expanded)) { return false; } /* Append to the "global" actions argv */ *argv = xrealloc(*argv, (*argc + count) * sizeof((*argv)[0])); memcpy(&(*argv)[*argc], expanded, count * sizeof(expanded[0])); *argc += count; free(expanded); return true; } bool notify_notify(struct terminal *term, struct notification *notif) { xassert(notif->xdg_token == NULL); xassert(notif->external_id == 0); xassert(notif->pid == 0); xassert(notif->stdout_fd <= 0); xassert(notif->stdout_data == NULL); xassert(notif->icon_path == NULL); xassert(notif->icon_fd <= 0); notif->pid = -1; notif->stdout_fd = -1; notif->icon_fd = -1; if (term->conf->desktop_notifications.command.argv.args == NULL) return false; if ((term->conf->desktop_notifications.inhibit_when_focused || notif->when != NOTIFY_ALWAYS) && term->kbd_focus) { /* No notifications while we're focused */ return false; } const char *app_id = notif->app_id != NULL ? notif->app_id : term->app_id != NULL ? term->app_id : term->conf->app_id; const char *title = notif->title != NULL ? notif->title : notif->body; const char *body = notif->title != NULL && notif->body != NULL ? notif->body : ""; /* Icon: symbolic name if present, otherwise a filename */ const char *icon_name_or_path = ""; if (notif->icon_cache_id != NULL) { for (size_t i = 0; i < ALEN(term->notification_icons); i++) { const struct notification_icon *icon = &term->notification_icons[i]; if (icon->id != NULL && streq(icon->id, notif->icon_cache_id)) { /* For now, we set the symbolic name to 'file:///path' * when using a file based icon. */ xassert(icon->symbolic_name != NULL); icon_name_or_path = icon->symbolic_name; LOG_DBG("using icon from cache (cache ID: %s): %s", icon->id, icon_name_or_path); break; } } } else if (notif->icon_symbolic_name != NULL) { icon_name_or_path = notif->icon_symbolic_name; LOG_DBG("using symbolic icon from notification: %s", icon_name_or_path); } else if (notif->icon_data_sz > 0) { xassert(notif->icon_data != NULL); if (write_icon_file( notif->icon_data, notif->icon_data_sz, ¬if->icon_fd, ¬if->icon_path, ¬if->icon_symbolic_name)) icon_name_or_path = notif->icon_symbolic_name; LOG_DBG("using icon data from notification: %s", icon_name_or_path); } bool track_notification = notif->focus || notif->report_activated || notif->may_be_programatically_closed; uint32_t replaces_id = 0; if (notif->id != NULL) { tll_foreach(term->active_notifications, it) { struct notification *existing = &it->item; if (existing->id == NULL) continue; /* * When replacing/updating a notification, we may have * *multiple* notification helpers running for the "same" * notification. Make sure only the *last* notification's * report closed/activated are honored, to avoid sending * multiple reports. * * This also means we cannot 'break' out of the loop - we * must check *all* notifications. */ if (existing->external_id != 0 && streq(existing->id, notif->id)) { replaces_id = existing->external_id; existing->report_activated = false; existing->report_closed = false; } } } char replaces_id_str[16]; xsnprintf(replaces_id_str, sizeof(replaces_id_str), "%u", replaces_id); const char *urgency_str = notif->urgency == NOTIFY_URGENCY_LOW ? "low" : notif->urgency == NOTIFY_URGENCY_NORMAL ? "normal" : "critical"; LOG_DBG("notify: title=\"%s\", body=\"%s\", app-id=\"%s\", category=\"%s\", " "urgency=\"%s\", icon=\"%s\", expires=%d, replaces=%u, muted=%s, " "sound-name=%s (tracking: %s)", title, body, app_id, notif->category, urgency_str, icon_name_or_path, notif->expire_time, replaces_id, notif->muted ? "yes" : "no", notif->sound_name, track_notification ? "yes" : "no"); xassert(title != NULL); if (title == NULL) return false; char **argv = NULL; size_t argc = 0; char **action_argv = NULL; size_t action_argc = 0; char expire_time[16]; xsnprintf(expire_time, sizeof(expire_time), "%d", notif->expire_time); if (term->conf->desktop_notifications.command_action_arg.argv.args) { if (!expand_action_to_argv( term, "default", "Activate", &action_argc, &action_argv)) { return false; } size_t action_idx = 1; tll_foreach(notif->actions, it) { /* Custom actions use a numerical name, starting at 1 */ char name[16]; xsnprintf(name, sizeof(name), "%zu", action_idx++); if (!expand_action_to_argv( term, name, it->item, &action_argc, &action_argv)) { for (size_t i = 0; i < action_argc; i++) free(action_argv[i]); free(action_argv); return false; } } } if (!spawn_expand_template( &term->conf->desktop_notifications.command, 12, (const char *[]){ "app-id", "window-title", "icon", "title", "body", "category", "urgency", "muted", "sound-name", "expire-time", "replace-id", "action-argument"}, (const char *[]){ app_id, term->window_title, icon_name_or_path, title, body != NULL ? body : "", notif->category != NULL ? notif->category : "", urgency_str, notif->muted ? "true" : "false", notif->sound_name != NULL ? notif->sound_name : "", expire_time, replaces_id_str, /* Custom expansion below, since we need to expand to multiple arguments */ "${action-argument}"}, &argc, &argv)) { return false; } /* Post-process the expanded argv, and patch in all the --action arguments we expanded earlier */ for (size_t i = 0; i < argc; i++) { if (!streq(argv[i], "${action-argument}")) continue; if (action_argc == 0) { free(argv[i]); /* Remove ${command-argument}, but include terminating NULL */ memmove(&argv[i], &argv[i + 1], (argc - i) * sizeof(argv[0])); argc--; break; } /* Remove the "${action-argument}" entry, add all actions argument from earlier, but include terminating NULL */ argv = xrealloc(argv, (argc + action_argc) * sizeof(argv[0])); /* Move remaining arguments to after the action arguments */ memmove(&argv[i + action_argc], &argv[i + 1], (argc - i) * sizeof(argv[0])); /* Include terminating NULL */ free(argv[i]); /* Free xstrdup("${action-argument}"); */ /* Insert the action arguments */ for (size_t j = 0; j < action_argc; j++) { argv[i + j] = action_argv[j]; action_argv[j] = NULL; } argc += action_argc; argc--; /* The ${action-argument} option has been removed */ break; } LOG_DBG("notify command:"); for (size_t i = 0; i < argc; i++) LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]); xassert(argv[argc] == NULL); int stdout_fds[2] = {-1, -1}; if (track_notification) { if (pipe2(stdout_fds, O_CLOEXEC | O_NONBLOCK) < 0) { LOG_WARN("failed to create stdout pipe"); track_notification = false; /* Non-fatal */ } else { tll_push_back(term->active_notifications, *notif); /* We've taken over ownership of all data; clear, so that notify_free() doesn't double free */ notif->id = NULL; notif->title = NULL; notif->body = NULL; notif->category = NULL; notif->app_id = NULL; notif->icon_cache_id = NULL; notif->icon_symbolic_name = NULL; notif->icon_data = NULL; notif->icon_data_sz = 0; notif->icon_path = NULL; notif->sound_name = NULL; notif->icon_fd = -1; notif->stdout_fd = -1; struct notification *new_notif = &tll_back(term->active_notifications); /* We don't need these anymore. They'll be free:d by the caller */ new_notif->button_count = tll_length(notif->actions); memset(&new_notif->actions, 0, sizeof(new_notif->actions)); notif = new_notif; } } if (stdout_fds[0] >= 0) { fdm_add(term->fdm, stdout_fds[0], EPOLLIN, &fdm_notify_stdout, (void *)term); } /* Redirect stdin to /dev/null, but ignore failure to open */ int devnull = open("/dev/null", O_RDONLY); pid_t pid = spawn( term->reaper, NULL, argv, devnull, stdout_fds[1], -1, track_notification ? ¬if_done : NULL, (void *)term, NULL); if (stdout_fds[1] >= 0) { /* Close write-end of stdout pipe */ close(stdout_fds[1]); } if (pid < 0 && stdout_fds[0] >= 0) { /* Remove FDM callback if we failed to spawn */ fdm_del(term->fdm, stdout_fds[0]); } if (devnull >= 0) close(devnull); for (size_t i = 0; i < argc; i++) free(argv[i]); free(argv); for (size_t i = 0; i < action_argc; i++) free(action_argv[i]); free(action_argv); notif->pid = pid; notif->stdout_fd = stdout_fds[0]; return true; } void notify_close(struct terminal *term, const char *id) { xassert(id != NULL); LOG_DBG("close notification %s", id); tll_foreach(term->active_notifications, it) { const struct notification *notif = &it->item; if (notif->id == NULL || !streq(notif->id, id)) continue; if (term->conf->desktop_notifications.close.argv.args == NULL) { LOG_DBG( "trying to close notification \"%s\" by sending SIGINT to %u", id, notif->pid); if (notif->pid == 0) { LOG_WARN( "cannot close notification \"%s\": no helper process running", id); } else { /* Best-effort... */ kill(notif->pid, SIGINT); } } else { LOG_DBG( "trying to close notification \"%s\" " "by running user defined command", id); if (notif->external_id == 0) { LOG_WARN("cannot close notification \"%s\": " "no daemon assigned notification ID available", id); return; } char **argv = NULL; size_t argc = 0; char external_id[16]; xsnprintf(external_id, sizeof(external_id), "%u", notif->external_id); if (!spawn_expand_template( &term->conf->desktop_notifications.close, 1, (const char *[]){"id"}, (const char *[]){external_id}, &argc, &argv)) { return; } int devnull = open("/dev/null", O_RDONLY); spawn( term->reaper, NULL, argv, devnull, -1, -1, NULL, (void *)term, NULL); if (devnull >= 0) close(devnull); for (size_t i = 0; i < argc; i++) free(argv[i]); free(argv); } return; } LOG_WARN("cannot close notification \"%s\": no such notification", id); } static void add_icon(struct notification_icon *icon, const char *id, const char *symbolic_name, const uint8_t *data, size_t data_sz) { icon->id = xstrdup(id); icon->symbolic_name = symbolic_name != NULL ? xstrdup(symbolic_name) : NULL; icon->tmp_file_name = NULL; icon->tmp_file_fd = -1; /* * Dump in-line data to a temporary file. This allows us to pass * the filename as a parameter to notification helpers * (i.e. notify-send -i ). * * Optimization: since we always prefer (i.e. use) the symbolic * name if present, there's no need to create a file on disk if we * have a symbolic name. */ if (symbolic_name == NULL && data_sz > 0) { write_icon_file( data, data_sz, &icon->tmp_file_fd, &icon->tmp_file_name, &icon->symbolic_name); } LOG_DBG("added icon to cache: ID=%s: sym=%s, file=%s", icon->id, icon->symbolic_name, icon->tmp_file_name); } void notify_icon_add(struct terminal *term, const char *id, const char *symbolic_name, const uint8_t *data, size_t data_sz) { #if defined(_DEBUG) for (size_t i = 0; i < ALEN(term->notification_icons); i++) { struct notification_icon *icon = &term->notification_icons[i]; if (icon->id != NULL && streq(icon->id, id)) { BUG("notification icon cache already contains \"%s\"", id); } } #endif for (size_t i = 0; i < ALEN(term->notification_icons); i++) { struct notification_icon *icon = &term->notification_icons[i]; if (icon->id == NULL) { add_icon(icon, id, symbolic_name, data, data_sz); return; } } /* Cache full - throw out first entry, add new entry last */ notify_icon_free(&term->notification_icons[0]); memmove(&term->notification_icons[0], &term->notification_icons[1], ((ALEN(term->notification_icons) - 1) * sizeof(term->notification_icons[0]))); add_icon( &term->notification_icons[ALEN(term->notification_icons) - 1], id, symbolic_name, data, data_sz); } void notify_icon_del(struct terminal *term, const char *id) { for (size_t i = 0; i < ALEN(term->notification_icons); i++) { struct notification_icon *icon = &term->notification_icons[i]; if (icon->id == NULL || !streq(icon->id, id)) continue; LOG_DBG("expelled %s from the notification icon cache", icon->id); notify_icon_free(icon); return; } } void notify_icon_free(struct notification_icon *icon) { if (icon->tmp_file_name != NULL) { unlink(icon->tmp_file_name); if (icon->tmp_file_fd >= 0) close(icon->tmp_file_fd); } free(icon->id); free(icon->symbolic_name); free(icon->tmp_file_name); icon->id = NULL; icon->symbolic_name = NULL; icon->tmp_file_name = NULL; icon->tmp_file_fd = -1; } foot-1.21.0/notify.h000066400000000000000000000054221476600145200142330ustar00rootroot00000000000000#pragma once #include #include #include #include struct terminal; enum notify_when { /* First, so that it can be left out of initializer and still be the default */ NOTIFY_ALWAYS, NOTIFY_UNFOCUSED, NOTIFY_INVISIBLE }; enum notify_urgency { /* First, so that it can be left out of initializer and still be the default */ NOTIFY_URGENCY_NORMAL, NOTIFY_URGENCY_LOW, NOTIFY_URGENCY_CRITICAL, }; struct notification { /* * Set by caller of notify_notify() */ char *id; /* Internal notification ID */ char *app_id; /* Custom app-id, overrides the terminal's app-id if set */ char *title; /* Required */ char *body; char *category; enum notify_when when; enum notify_urgency urgency; int32_t expire_time; tll(char *) actions; char *icon_cache_id; char *icon_symbolic_name; uint8_t *icon_data; size_t icon_data_sz; bool focus; /* Focus the foot window when notification is activated */ bool may_be_programatically_closed; /* OSC-99: notification may be programmatically closed by the client */ bool report_activated; /* OSC-99: report notification activation to client */ bool report_closed; /* OSC-99: report notification closed to client */ bool muted; /* Explicitly mute the notification */ char *sound_name; /* Should be set to NULL if muted == true */ /* * Used internally by notify */ uint32_t external_id; /* Daemon assigned notification ID */ bool activated; /* User 'activated' the notification */ uint32_t button_count; /* Number of buttons (custom actions) in notification */ uint32_t activated_button; /* User activated one of the custom actions */ char *xdg_token; /* XDG activation token, from daemon */ pid_t pid; /* Notifier command PID */ int stdout_fd; /* Notifier command's stdout */ char *stdout_data; /* Data we've reado from command's stdout */ size_t stdout_sz; /* Used when notification provides raw icon data, and it's bypassing the icon cache */ char *icon_path; int icon_fd; }; struct notification_icon { char *id; char *symbolic_name; char *tmp_file_name; int tmp_file_fd; }; bool notify_notify(struct terminal *term, struct notification *notif); void notify_close(struct terminal *term, const char *id); void notify_free(struct terminal *term, struct notification *notif); void notify_icon_add(struct terminal *term, const char *id, const char *symbolic_name, const uint8_t *data, size_t data_sz); void notify_icon_del(struct terminal *term, const char *id); void notify_icon_free(struct notification_icon *icon); foot-1.21.0/org.codeberg.dnkl.foot.metainfo.xml000066400000000000000000000043731476600145200213360ustar00rootroot00000000000000 org.codeberg.dnkl.foot MIT MIT dnkl foot The fast, lightweight and minimalistic Wayland terminal emulator.
  • Fast
  • Lightweight, in dependencies, on-disk and in-memory
  • Wayland native
  • DE agnostic
  • Server/daemon mode
  • User configurable font fallback
  • On-the-fly font resize
  • On-the-fly DPI font size adjustment
  • Scrollback search
  • Keyboard driven URL detection
  • Color emoji support
  • IME (via text-input-v3)
  • Multi-seat
  • True Color (24bpp)
  • Styled and colored underlines
  • Synchronized Updates support
  • Sixel image support
Foot with sixel graphics https://codeberg.org/dnkl/foot/media/branch/master/doc/sixel-wow.png org.codeberg.dnkl.foot.desktop https://codeberg.org/dnkl/foot https://codeberg.org/dnkl/foot/issues
foot-1.21.0/osc.c000066400000000000000000001405651476600145200135120ustar00rootroot00000000000000#include "osc.h" #include #include #include #include #include #define LOG_MODULE "osc" #define LOG_ENABLE_DBG 0 #include "log.h" #include "base64.h" #include "config.h" #include "macros.h" #include "notify.h" #include "selection.h" #include "terminal.h" #include "uri.h" #include "util.h" #include "xmalloc.h" #include "xsnprintf.h" #define UNHANDLED() LOG_DBG("unhandled: OSC: %.*s", (int)term->vt.osc.idx, term->vt.osc.data) static void osc_to_clipboard(struct terminal *term, const char *target, const char *base64_data) { bool to_clipboard = false; bool to_primary = false; if (target[0] == '\0') to_clipboard = true; for (const char *t = target; *t != '\0'; t++) { switch (*t) { case 'c': to_clipboard = true; break; case 's': case 'p': to_primary = true; break; default: LOG_WARN("unimplemented: clipboard target '%c'", *t); break; } } /* Find a seat in which the terminal has focus */ struct seat *seat = NULL; tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) { seat = &it->item; break; } } if (seat == NULL) { LOG_WARN("OSC52: client tried to write to clipboard data while window was unfocused"); return; } const bool copy_allowed = term->conf->security.osc52 == OSC52_ENABLED || term->conf->security.osc52 == OSC52_COPY_ENABLED; if (!copy_allowed) { LOG_DBG("ignoring copy request: disabled in configuration"); return; } char *decoded = base64_decode(base64_data, NULL); if (decoded == NULL) { 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); return; } LOG_DBG("decoded: %s", decoded); if (to_clipboard) { char *copy = xstrdup(decoded); if (!text_to_clipboard(seat, term, copy, seat->kbd.serial)) free(copy); } if (to_primary) { char *copy = xstrdup(decoded); if (!text_to_primary(seat, term, copy, seat->kbd.serial)) free(copy); } free(decoded); } struct clip_context { struct seat *seat; struct terminal *term; uint8_t buf[3]; int idx; }; static void from_clipboard_cb(char *text, size_t size, void *user) { struct clip_context *ctx = user; struct terminal *term = ctx->term; xassert(ctx->idx >= 0 && ctx->idx <= 2); const char *t = text; size_t left = size; if (ctx->idx > 0) { for (size_t i = ctx->idx; i < 3 && left > 0; i++, t++, left--) ctx->buf[ctx->idx++] = *t; xassert(ctx->idx <= 3); if (ctx->idx == 3) { char *chunk = base64_encode(ctx->buf, 3); xassert(chunk != NULL); xassert(strlen(chunk) == 4); term_paste_data_to_slave(term, chunk, 4); free(chunk); ctx->idx = 0; } } if (left == 0) return; xassert(ctx->idx == 0); int remaining = left % 3; for (int i = remaining; i > 0; i--) ctx->buf[ctx->idx++] = text[size - i]; xassert(ctx->idx == remaining); char *chunk = base64_encode((const uint8_t *)t, left / 3 * 3); xassert(chunk != NULL); xassert(strlen(chunk) % 4 == 0); term_paste_data_to_slave(term, chunk, strlen(chunk)); free(chunk); } static void from_clipboard_done(void *user) { struct clip_context *ctx = user; struct terminal *term = ctx->term; if (ctx->idx > 0) { char res[4]; base64_encode_final(ctx->buf, ctx->idx, res); term_paste_data_to_slave(term, res, 4); } if (term->vt.osc.bel) term_paste_data_to_slave(term, "\a", 1); else term_paste_data_to_slave(term, "\033\\", 2); term->is_sending_paste_data = false; /* Make sure we send any queued up non-paste data */ if (tll_length(term->ptmx_buffers) > 0) fdm_event_add(term->fdm, term->ptmx, EPOLLOUT); free(ctx); } static void osc_from_clipboard(struct terminal *term, const char *source) { /* Find a seat in which the terminal has focus */ struct seat *seat = NULL; tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) { seat = &it->item; break; } } if (seat == NULL) { LOG_WARN("OSC52: client tried to read clipboard data while window was unfocused"); return; } const bool paste_allowed = term->conf->security.osc52 == OSC52_ENABLED || term->conf->security.osc52 == OSC52_PASTE_ENABLED; if (!paste_allowed) { LOG_DBG("ignoring paste request: disabled in configuration"); return; } /* Use clipboard if no source has been specified */ char src = source[0] == '\0' ? 'c' : 0; bool from_clipboard = src == 'c'; bool from_primary = false; for (const char *s = source; *s != '\0' && !from_clipboard && !from_primary; s++) { if (*s == 'c' || *s == 'p' || *s == 's') { src = *s; switch (src) { case 'c': from_clipboard = selection_clipboard_has_data(seat); break; case 's': case 'p': from_primary = selection_primary_has_data(seat); break; } } else LOG_WARN("unimplemented: clipboard source '%c'", *s); } if (!from_clipboard && !from_primary) return; if (term->is_sending_paste_data) { /* FIXME: we should wait for the paste to end, then continue with the OSC-52 reply */ term_to_slave(term, "\033]52;", 5); term_to_slave(term, &src, 1); term_to_slave(term, ";", 1); if (term->vt.osc.bel) term_to_slave(term, "\a", 1); else term_to_slave(term, "\033\\", 2); return; } term->is_sending_paste_data = true; term_paste_data_to_slave(term, "\033]52;", 5); term_paste_data_to_slave(term, &src, 1); term_paste_data_to_slave(term, ";", 1); struct clip_context *ctx = xmalloc(sizeof(*ctx)); *ctx = (struct clip_context) {.seat = seat, .term = term}; if (from_clipboard) { text_from_clipboard( seat, term, &from_clipboard_cb, &from_clipboard_done, ctx); } if (from_primary) { text_from_primary( seat, term, &from_clipboard_cb, &from_clipboard_done, ctx); } } static void osc_selection(struct terminal *term, char *string) { char *p = string; bool clipboard_done = false; /* The first parameter is a string of clipbard sources/targets */ while (*p != '\0' && !clipboard_done) { switch (*p) { case ';': clipboard_done = true; *p = '\0'; break; } p++; } LOG_DBG("clipboard: target = %s data = %s", string, p); if (p[0] == '?' && p[1] == '\0') osc_from_clipboard(term, string); else osc_to_clipboard(term, string, p); } static void osc_flash(struct terminal *term) { /* Our own private - flash */ term_flash(term, 50); } static bool parse_legacy_color(const char *string, uint32_t *color, bool *_have_alpha, uint16_t *_alpha) { bool have_alpha = false; uint16_t alpha = 0xffff; if (string[0] == '[') { /* e.g. \E]11;[50]#00ff00 */ const char *start = &string[1]; errno = 0; char *end; unsigned long percent = strtoul(start, &end, 10); if (errno != 0 || *end != ']') return false; have_alpha = true; alpha = (0xffff * min(percent, 100) + 50) / 100; string = end + 1; } if (string[0] != '#') return false; string++; const size_t len = strlen(string); if (len % 3 != 0) return false; const int digits = len / 3; int rgb[3]; for (size_t i = 0; i < 3; i++) { rgb[i] = 0; for (size_t j = 0; j < digits; j++) { size_t idx = i * digits + j; char c = string[idx]; rgb[i] <<= 4; if (!isxdigit(c)) rgb[i] |= 0; else rgb[i] |= c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : c - 'A' + 10; } /* Values with less than 16 bits represent the *most * significant bits*. I.e. the values are *not* scaled */ rgb[i] <<= 16 - (4 * digits); } /* Re-scale to 8-bit */ uint8_t r = 256 * (rgb[0] / 65536.); uint8_t g = 256 * (rgb[1] / 65536.); uint8_t b = 256 * (rgb[2] / 65536.); LOG_DBG("legacy: %02x%02x%02x (alpha=%04x)", r, g, b, have_alpha ? alpha : 0xffff); *color = r << 16 | g << 8 | b; if (_have_alpha != NULL) *_have_alpha = have_alpha; if (_alpha != NULL) *_alpha = alpha; return true; } static bool parse_rgb(const char *string, uint32_t *color, bool *_have_alpha, uint16_t *_alpha) { size_t len = strlen(string); bool have_alpha = len >= 4 && strncmp(string, "rgba", 4) == 0; /* Verify we have the minimum required length (for "") */ if (have_alpha) { if (len < STRLEN("rgba:x/x/x/x")) return false; } else { if (len < STRLEN("rgb:x/x/x")) return false; } /* Verify prefix is "rgb:" or "rgba:" */ if (have_alpha) { if (strncmp(string, "rgba:", 5) != 0) return false; string += 5; len -= 5; } else { if (strncmp(string, "rgb:", 4) != 0) return false; string += 4; len -= 4; } int rgb[4]; int digits[4]; for (size_t i = 0; i < (have_alpha ? 4 : 3); i++) { for (rgb[i] = 0, digits[i] = 0; len > 0 && *string != '/'; len--, string++, digits[i]++) { char c = *string; rgb[i] <<= 4; if (!isxdigit(c)) rgb[i] |= 0; else rgb[i] |= c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : c - 'A' + 10; } if (i >= (have_alpha ? 3 : 2)) break; if (len == 0 || *string != '/') return false; string++; len--; } /* Re-scale to 8-bit */ uint8_t r = 256 * (rgb[0] / (double)(1 << (4 * digits[0]))); uint8_t g = 256 * (rgb[1] / (double)(1 << (4 * digits[1]))); uint8_t b = 256 * (rgb[2] / (double)(1 << (4 * digits[2]))); uint16_t alpha = 0xffff; if (have_alpha) alpha = 65536 * (rgb[3] / (double)(1 << (4 * digits[3]))); if (have_alpha) LOG_DBG("rgba: %02x%02x%02x (alpha=%04x)", r, g, b, alpha); else LOG_DBG("rgb: %02x%02x%02x", r, g, b); if (_have_alpha != NULL) *_have_alpha = have_alpha; if (_alpha != NULL) *_alpha = alpha; *color = r << 16 | g << 8 | b; return true; } static void osc_set_pwd(struct terminal *term, char *string) { LOG_DBG("PWD: URI: %s", string); char *scheme, *host, *path; if (!uri_parse(string, strlen(string), &scheme, NULL, NULL, &host, NULL, &path, NULL, NULL)) { LOG_ERR("OSC7: invalid URI: %s", string); return; } if (streq(scheme, "file") && hostname_is_localhost(host)) { LOG_DBG("OSC7: pwd: %s", path); free(term->cwd); term->cwd = path; } else free(path); free(scheme); free(host); } static void osc_uri(struct terminal *term, char *string) { /* * \E]8;;URI\e\\ * * Params are key=value pairs, separated by ':'. * * The only defined key (as of 2020-05-31) is 'id', which is used * to group split-up URIs: * * ╔═ file1 ════╗ * ║ ╔═ file2 ═══╗ * ║http://exa║Lorem ipsum║ * ║le.com ║ dolor sit ║ * ║ ║amet, conse║ * ╚══════════║ctetur adip║ * ╚═══════════╝ * * This lets a terminal emulator highlight both parts at the same * time (e.g. when hovering over one of the parts with the mouse). */ char *params = string; char *params_end = strchr(params, ';'); if (params_end == NULL) return; *params_end = '\0'; const char *uri = params_end + 1; uint64_t id = (uint64_t)rand() << 32 | rand(); char *ctx = NULL; for (const char *key_value = strtok_r(params, ":", &ctx); key_value != NULL; key_value = strtok_r(NULL, ":", &ctx)) { const char *key = key_value; char *operator = strchr(key_value, '='); if (operator == NULL) continue; *operator = '\0'; const char *value = operator + 1; if (streq(key, "id")) id = sdbm_hash(value); } LOG_DBG("OSC-8: URL=%s, id=%" PRIu64, uri, id); if (uri[0] == '\0') term_osc8_close(term); else term_osc8_open(term, id, uri); } static void osc_notify(struct terminal *term, char *string) { /* * The 'notify' perl extension * (https://pub.phyks.me/scripts/urxvt/notify) is very simple: * * #!/usr/bin/perl * * sub on_osc_seq_perl { * my ($term, $osc, $resp) = @_; * if ($osc =~ /^notify;(\S+);(.*)$/) { * system("notify-send '$1' '$2'"); * } * } * * As can be seen, the notification text is not encoded in any * way. The regex does a greedy match of the ';' separator. Thus, * any extra ';' will end up being part of the title. There's no * way to have a ';' in the message body. * * I've changed that behavior slightly in; we split the title from * body on the *first* ';', allowing us to have semicolons in the * message body, but *not* in the title. */ char *ctx = NULL; const char *title = strtok_r(string, ";", &ctx); const char *msg = strtok_r(NULL, "\x00", &ctx); if (title == NULL) return; if (mbsntoc32(NULL, title, strlen(title), 0) == (size_t)-1) { LOG_WARN("%s: notification title is not valid UTF-8, ignoring", title); return; } if (msg != NULL && mbsntoc32(NULL, msg, strlen(msg), 0) == (size_t)-1) { LOG_WARN("%s: notification message is not valid UTF-8, ignoring", msg); return; } char *msgdup = NULL; if (msg != NULL) msgdup = xstrdup(msg); notify_notify(term, &(struct notification){ .title = xstrdup(title), .body = msgdup, .expire_time = -1, .focus = true, }); } IGNORE_WARNING("-Wpedantic") static bool verify_kitty_id_is_valid(const char *id) { const size_t len = strlen(id); for (size_t i = 0; i < len; i++) { switch (id[i]) { case 'a' ... 'z': case 'A' ... 'Z': case '0' ... '9': case '_': case '-': case '+': case '.': break; default: return false; } } return true; } UNIGNORE_WARNINGS static void kitty_notification(struct terminal *term, char *string) { /* https://sw.kovidgoyal.net/kitty/desktop-notifications */ char *payload_raw = strchr(string, ';'); if (payload_raw == NULL) return; char *parameters = string; *payload_raw = '\0'; payload_raw++; char *id = NULL; /* The 'i' parameter */ char *app_id = NULL; /* The 'f' parameter */ char *icon_cache_id = NULL; /* The 'g' parameter */ char *symbolic_icon = NULL; /* The 'n' parameter */ char *category = NULL; /* The 't' parameter */ char *sound_name = NULL; /* The 's' parameter */ char *payload = NULL; bool focus = true; /* The 'a' parameter */ bool report_activated = false; /* The 'a' parameter */ bool report_closed = false; /* The 'c' parameter */ bool done = true; /* The 'd' parameter */ bool base64 = false; /* The 'e' parameter */ int32_t expire_time = -1; /* The 'w' parameter */ size_t payload_size; enum { PAYLOAD_TITLE, PAYLOAD_BODY, PAYLOAD_CLOSE, PAYLOAD_ALIVE, PAYLOAD_ICON, PAYLOAD_BUTTON, } payload_type = PAYLOAD_TITLE; /* The 'p' parameter */ enum notify_when when = NOTIFY_ALWAYS; enum notify_urgency urgency = NOTIFY_URGENCY_NORMAL; bool have_a = false; bool have_c = false; bool have_o = false; bool have_u = false; bool have_w = false; char *ctx = NULL; for (char *param = strtok_r(parameters, ":", &ctx); param != NULL; param = strtok_r(NULL, ":", &ctx)) { /* All parameters are on the form X=value, where X is always exactly one character */ if (param[0] == '\0' || param[1] != '=') continue; char *value = ¶m[2]; switch (param[0]) { case 'a': { /* notification activation action: focus|report|-focus|-report */ have_a = true; char *a_ctx = NULL; for (const char *v = strtok_r(value, ",", &a_ctx); v != NULL; v = strtok_r(NULL, ",", &a_ctx)) { bool reverse = v[0] == '-'; if (reverse) v++; if (streq(v, "focus")) focus = !reverse; else if (streq(v, "report")) report_activated = !reverse; } break; } case 'c': if (value[0] == '1' && value[1] == '\0') report_closed = true; else if (value[0] == '0' && value[1] == '\0') report_closed = false; have_c = true; break; case 'd': /* done: 0|1 */ if (value[0] == '0' && value[1] == '\0') done = false; else if (value[0] == '1' && value[1] == '\0') done = true; break; case 'e': /* base64 (payload encoding): 0=utf8, 1=base64(utf8) */ if (value[0] == '0' && value[1] == '\0') base64 = false; else if (value[0] == '1' && value[1] == '\0') base64 = true; break; case 'i': /* id */ if (verify_kitty_id_is_valid(value)) { free(id); id = xstrdup(value); } else LOG_WARN("OSC-99: ignoring invalid 'i' identifier"); break; case 'p': /* payload content: title|body */ if (streq(value, "title")) payload_type = PAYLOAD_TITLE; else if (streq(value, "body")) payload_type = PAYLOAD_BODY; else if (streq(value, "close")) payload_type = PAYLOAD_CLOSE; else if (streq(value, "alive")) payload_type = PAYLOAD_ALIVE; else if (streq(value, "icon")) payload_type = PAYLOAD_ICON; else if (streq(value, "buttons")) payload_type = PAYLOAD_BUTTON; else if (streq(value, "?")) { /* Query capabilities */ const char *reply_id = id != NULL ? id : "0"; const char *p_caps = "title,body,?,close,alive,icon,buttons"; const char *a_caps = "focus,report"; const char *u_caps = "0,1,2"; char when_caps[64]; strcpy(when_caps, "unfocused"); if (!term->conf->desktop_notifications.inhibit_when_focused) strcat(when_caps, ",always"); const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; char reply[128]; size_t n = xsnprintf( reply, sizeof(reply), "\033]99;i=%s:p=?;p=%s:a=%s:o=%s:u=%s:c=1:w=1:s=system,silent,error,warn,warning,info,question%s", reply_id, p_caps, a_caps, when_caps, u_caps, terminator); xassert(n < sizeof(reply)); term_to_slave(term, reply, n); goto out; } break; case 'o': /* honor when: always|unfocused|invisible */ have_o = true; if (streq(value, "always")) when = NOTIFY_ALWAYS; else if (streq(value, "unfocused")) when = NOTIFY_UNFOCUSED; else if (streq(value, "invisible")) when = NOTIFY_INVISIBLE; break; case 'u': /* urgency: 0=low, 1=normal, 2=critical */ have_u = true; if (value[0] == '0' && value[1] == '\0') urgency = NOTIFY_URGENCY_LOW; else if (value[0] == '1' && value[1] == '\0') urgency = NOTIFY_URGENCY_NORMAL; else if (value[0] == '2' && value[1] == '\0') urgency = NOTIFY_URGENCY_CRITICAL; break; case 'w': { /* Notification timeout */ errno = 0; char *end = NULL; long timeout = strtol(value, &end, 10); if (errno == 0 && *end == '\0' && timeout <= INT32_MAX) { expire_time = timeout; have_w = true; } break; } case 'f': { /* App-name */ char *decoded = base64_decode(value, NULL); if (decoded != NULL) { free(app_id); app_id = decoded; } break; } case 't': { /* Type (category) */ char *decoded = base64_decode(value, NULL); if (decoded != NULL) { if (category == NULL) category = decoded; else { /* Append, comma separated */ char *old_category = category; category = xstrjoin3(old_category, ",", decoded); free(decoded); free(old_category); } } break; } case 's': { /* Sound */ char *decoded = base64_decode(value, NULL); if (decoded != NULL) { free(sound_name); sound_name = decoded; const char *translated_name = NULL; if (streq(decoded, "error")) translated_name = "dialog-error"; else if (streq(decoded, "warn") || streq(decoded, "warning")) translated_name = "dialog-warning"; else if (streq(decoded, "info")) translated_name = "dialog-information"; else if (streq(decoded, "question")) translated_name = "dialog-question"; if (translated_name != NULL) { free(sound_name); sound_name = xstrdup(translated_name); } } break; } case 'g': /* graphical ID (see 'n' and 'p=icon') */ free(icon_cache_id); icon_cache_id = xstrdup(value); break; case 'n': { /* Symbolic icon name, may used with 'g' */ /* * Sigh, protocol says 'n' can be used multiple times, and * that the terminal picks the first one that it can * resolve. * * We can't resolve any icons at all. So, enter * heuristics... let's pick the *shortest* symbolic * name. The idea is that icon *names* are typically * shorter than .desktop names, and macOS bundle * identifiers. */ char *maybe_new_symbolic_icon = base64_decode(value, NULL); if (maybe_new_symbolic_icon == NULL) break; if (symbolic_icon == NULL || strlen(maybe_new_symbolic_icon) < strlen(symbolic_icon)) { free(symbolic_icon); symbolic_icon = maybe_new_symbolic_icon; /* Translate OSC-99 "special" names */ if (symbolic_icon != NULL) { const char *translated_name = NULL; if (streq(symbolic_icon, "error")) translated_name = "dialog-error"; else if (streq(symbolic_icon, "warn") || streq(symbolic_icon, "warning")) translated_name = "dialog-warning"; else if (streq(symbolic_icon, "info")) translated_name = "dialog-information"; else if (streq(symbolic_icon, "question")) translated_name = "dialog-question"; else if (streq(symbolic_icon, "help")) translated_name = "system-help"; else if (streq(symbolic_icon, "file-manager")) translated_name = "system-file-manager"; else if (streq(symbolic_icon, "system-monitor")) translated_name = "utilities-system-monitor"; else if (streq(symbolic_icon, "text-editor")) translated_name = "text-editor"; if (translated_name != NULL) { free(symbolic_icon); symbolic_icon = xstrdup(translated_name); } } } else { free(maybe_new_symbolic_icon); } break; } } } if (base64) { payload = base64_decode(payload_raw, &payload_size); if (payload == NULL) goto out; } else { payload = xstrdup(payload_raw); payload_size = strlen(payload); } /* Append metadata to previous notification chunk */ struct notification *notif = &term->kitty_notification; if (!((id == NULL && notif->id == NULL) || (id != NULL && notif->id != NULL && streq(id, notif->id))) || !notif->may_be_programatically_closed) /* Free:d notification has this as false... */ { /* ID mismatch, ignore previous notification state */ notify_free(term, notif); notif->id = id; notif->when = when; notif->urgency = urgency; notif->expire_time = expire_time; notif->focus = focus; notif->may_be_programatically_closed = true; notif->report_activated = report_activated; notif->report_closed = report_closed; id = NULL; /* Prevent double free */ } if (have_a) { notif->focus = focus; notif->report_activated = report_activated; } if (have_c) notif->report_closed = report_closed; if (have_o) notif->when = when; if (have_u) notif->urgency = urgency; if (have_w) notif->expire_time = expire_time; if (icon_cache_id != NULL) { free(notif->icon_cache_id); notif->icon_cache_id = icon_cache_id; icon_cache_id = NULL; /* Prevent double free */ } if (symbolic_icon != NULL) { free(notif->icon_symbolic_name); notif->icon_symbolic_name = symbolic_icon; symbolic_icon = NULL; } if (app_id != NULL) { free(notif->app_id); notif->app_id = app_id; app_id = NULL; /* Prevent double free */ } if (category != NULL) { if (notif->category == NULL) { notif->category = category; category = NULL; /* Prevent double free */ } else { /* Append, comma separated */ char *new_category = xstrjoin3(notif->category, ",", category); free(notif->category); notif->category = new_category; } } if (sound_name != NULL) { notif->muted = streq(sound_name, "silent"); if (notif->muted || streq(sound_name, "system")) { free(notif->sound_name); notif->sound_name = NULL; } else { free(notif->sound_name); notif->sound_name = sound_name; sound_name = NULL; /* Prevent double free */ } } /* Handled chunked payload - append to existing metadata */ switch (payload_type) { case PAYLOAD_TITLE: case PAYLOAD_BODY: { char **ptr = payload_type == PAYLOAD_TITLE ? ¬if->title : ¬if->body; if (*ptr == NULL) { *ptr = payload; payload = NULL; } else { char *old = *ptr; *ptr = xstrjoin(old, payload); free(old); } break; } case PAYLOAD_CLOSE: case PAYLOAD_ALIVE: /* Ignore payload */ break; case PAYLOAD_ICON: if (notif->icon_data == NULL) { notif->icon_data = (uint8_t *)payload; notif->icon_data_sz = payload_size; payload = NULL; } else { notif->icon_data = xrealloc( notif->icon_data, notif->icon_data_sz + payload_size); memcpy(¬if->icon_data[notif->icon_data_sz], payload, payload_size); notif->icon_data_sz += payload_size; } break; case PAYLOAD_BUTTON: { char *ctx = NULL; for (const char *button = strtok_r(payload, "\u2028", &ctx); button != NULL; button = strtok_r(NULL, "\u2028", &ctx)) { if (button[0] != '\0') { tll_push_back(notif->actions, xstrdup(button)); } } break; } } if (done) { /* Update icon cache, if necessary */ if (notif->icon_cache_id != NULL && (notif->icon_symbolic_name != NULL || notif->icon_data != NULL)) { notify_icon_del(term, notif->icon_cache_id); notify_icon_add(term, notif->icon_cache_id, notif->icon_symbolic_name, notif->icon_data, notif->icon_data_sz); /* Don't need this anymore */ free(notif->icon_symbolic_name); free(notif->icon_data); notif->icon_symbolic_name = NULL; notif->icon_data = NULL; notif->icon_data_sz = 0; } if (payload_type == PAYLOAD_CLOSE) { if (notif->id != NULL) notify_close(term, notif->id); } else if (payload_type == PAYLOAD_ALIVE) { char *alive_ids = NULL; tll_foreach(term->active_notifications, it) { /* TODO: check with kitty: use "0" for all notifications with no ID? */ const char *item_id = it->item.id != NULL ? it->item.id : "0"; if (alive_ids == NULL) alive_ids = xstrdup(item_id); else { char *old_alive_ids = alive_ids; alive_ids = xstrjoin3(old_alive_ids, ",", item_id); free(old_alive_ids); } } char *reply = xasprintf( "\033]99;i=%s:p=alive;%s\033\\", notif->id != NULL ? notif->id : "0", alive_ids != NULL ? alive_ids : ""); term_to_slave(term, reply, strlen(reply)); free(reply); free(alive_ids); } else { /* * Show notification. * * The checks for title|body is to handle notifications that * only load icon data into the icon cache */ if (notif->title != NULL || notif->body != NULL) { notify_notify(term, notif); } } notify_free(term, notif); } out: free(id); free(app_id); free(icon_cache_id); free(symbolic_icon); free(payload); free(category); free(sound_name); } static void kitty_text_size(struct terminal *term, char *string) { char *text = strchr(string, ';'); if (text == NULL) return; char *parameters = string; *text = '\0'; text++; char32_t *wchars = ambstoc32(text); if (wchars == NULL) return; int forced_width = 0; char *ctx = NULL; for (char *param = strtok_r(parameters, ":", &ctx); param != NULL; param = strtok_r(NULL, ":", &ctx)) { /* All parameters are on the form X=value, where X is always exactly one character */ if (param[0] == '\0' || param[1] != '=') continue; char *value = ¶m[2]; switch (param[0]) { case 'w': { errno = 0; char *end = NULL; unsigned long w = strtoul(value, &end, 10); if (*end == '\0' && errno == 0 && w <= 7) { forced_width = (int)w; break; } else LOG_ERR("OSC-66: invalid 'w' value, ignoring"); break; } case 's': case 'n': case 'd': case 'v': LOG_WARN("OSC-66: unsupported: '%c' parameter, ignoring", param[0]); break; } } const size_t len = c32len(wchars); if (forced_width == 0) { /* * w=0 means we split the text up as we'd normally do... Since * we don't support any other parameters of the text-sizing * protocol, that means we just process the string as if it * has been printed without this OSC. */ for (size_t i = 0; i < len; i++) term_process_and_print_non_ascii(term, wchars[i]); free(wchars); return; } size_t max_cp_width = 0; size_t all_cp_width = 0; for (size_t i = 0; i < len; i++) { const size_t cp_width = c32width(wchars[i]); all_cp_width += cp_width; max_cp_width = max(max_cp_width, cp_width); } size_t calculated_width = 0; switch (term->conf->tweak.grapheme_width_method) { case GRAPHEME_WIDTH_WCSWIDTH: calculated_width = all_cp_width; break; case GRAPHEME_WIDTH_MAX: calculated_width = max_cp_width; break; case GRAPHEME_WIDTH_DOUBLE: calculated_width = min(max_cp_width, 2); break; } const size_t width = forced_width == 0 ? calculated_width : forced_width; LOG_DBG("len=%zu, forced=%d, calculated=%zu, using=%zu", len, forced_width, calculated_width, width); #if 0 if (len == 1 && calculated_width == forced_width) { /* * Optimization: if there's a single codepoint, and either * w=0, or the 'w' matches the calculated width, print * codepoint directly instead of creating a combining * character. */ term_print(term, wchars[0], width); free(wchars); return; } #endif uint32_t key = composed_key_from_chars(wchars, len); const struct composed *composed = composed_lookup_without_collision( term->composed, &key, wchars, len - 1, wchars[len - 1], forced_width); if (composed == NULL) { struct composed *new_cc = xmalloc(sizeof(*new_cc)); new_cc->chars = wchars; new_cc->count = len; new_cc->key = key; new_cc->width = width; new_cc->forced_width = forced_width; term->composed_count++; composed_insert(&term->composed, new_cc); composed = new_cc; } else if (composed->width == width) { free(wchars); } term_print( term, CELL_COMB_CHARS_LO + composed->key, composed->forced_width > 0 ? composed->forced_width : composed->width, false); } void osc_dispatch(struct terminal *term) { unsigned param = 0; int data_ofs = 0; for (size_t i = 0; i < term->vt.osc.idx; i++, data_ofs++) { char c = term->vt.osc.data[i]; if (c == ';') { data_ofs++; break; } if (!isdigit(c)) { UNHANDLED(); return; } param *= 10; param += c - '0'; } LOG_DBG("OSC: %.*s (param = %d)", (int)term->vt.osc.idx, term->vt.osc.data, param); char *string = (char *)&term->vt.osc.data[data_ofs]; switch (param) { case 0: /* icon + title */ term_set_window_title(term, string); break; case 1: /* icon */ break; case 2: /* title */ term_set_window_title(term, string); break; case 4: { /* Set color */ string--; if (*string != ';') break; xassert(*string == ';'); for (const char *s_idx = strtok(string, ";"), *s_color = strtok(NULL, ";"); s_idx != NULL && s_color != NULL; s_idx = strtok(NULL, ";"), s_color = strtok(NULL, ";")) { /* Parse parameter */ unsigned idx = 0; for (; *s_idx != '\0'; s_idx++) { char c = *s_idx; idx *= 10; idx += c - '0'; } if (idx >= ALEN(term->colors.table)) { LOG_WARN("invalid OSC 4 color index: %u", idx); break; } /* Client queried for current value */ if (s_color[0] == '?' && s_color[1] == '\0') { uint32_t color = term->colors.table[idx]; uint8_t r = (color >> 16) & 0xff; uint8_t g = (color >> 8) & 0xff; uint8_t b = (color >> 0) & 0xff; const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; char reply[32]; size_t n = xsnprintf( reply, sizeof(reply), "\033]4;%u;rgb:%02hhx%02hhx/%02hhx%02hhx/%02hhx%02hhx%s", idx, r, r, g, g, b, b, terminator); term_to_slave(term, reply, n); } else { uint32_t color; bool color_is_valid = s_color[0] == '#' || s_color[0] == '[' ? parse_legacy_color(s_color, &color, NULL, NULL) : parse_rgb(s_color, &color, NULL, NULL); if (!color_is_valid) continue; LOG_DBG("change color definition for #%u from %06x to %06x", idx, term->colors.table[idx], color); term->colors.table[idx] = color; term_damage_color(term, COLOR_BASE256, idx); } } break; } case 7: /* Update terminal's understanding of PWD */ osc_set_pwd(term, string); break; case 8: osc_uri(term, string); break; case 9: { /* iTerm2 Growl notifications */ const char *sep = strchr(string, ';'); if (sep != NULL) { errno = 0; char *end = NULL; strtoul(string, &end, 10); if (end == sep && errno == 0) { /* Ignore ConEmu/Windows Terminal escape */ break; } } osc_notify(term, string); break; } case 10: /* fg */ case 11: /* bg */ case 12: /* cursor */ case 17: /* highlight (selection) fg */ case 19: { /* highlight (selection) bg */ /* Set default foreground/background/highlight-bg/highlight-fg color */ /* Client queried for current value */ if (string[0] == '?' && string[1] == '\0') { uint32_t color = param == 10 ? term->colors.fg : param == 11 ? term->colors.bg : param == 12 ? term->colors.cursor_bg : param == 17 ? term->colors.selection_bg : term->colors.selection_fg; uint8_t r = (color >> 16) & 0xff; uint8_t g = (color >> 8) & 0xff; uint8_t b = (color >> 0) & 0xff; const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; /* * Reply in XParseColor format * E.g. for color 0xdcdccc we reply "\033]10;rgb:dc/dc/cc\033\\" */ char reply[32]; size_t n = xsnprintf( reply, sizeof(reply), "\033]%u;rgb:%02hhx%02hhx/%02hhx%02hhx/%02hhx%02hhx%s", param, r, r, g, g, b, b, terminator); term_to_slave(term, reply, n); break; } uint32_t color; bool have_alpha = false; uint16_t alpha = 0xffff; if (string[0] == '#' || string[0] == '[' ? !parse_legacy_color(string, &color, &have_alpha, &alpha) : !parse_rgb(string, &color, &have_alpha, &alpha)) { break; } LOG_DBG("change color definition for %s to %06x", param == 10 ? "foreground" : param == 11 ? "background" : param == 12 ? "cursor" : param == 17 ? "selection background" : "selection foreground", color); switch (param) { case 10: term->colors.fg = color; term_damage_color(term, COLOR_DEFAULT, 0); break; case 11: term->colors.bg = color; if (have_alpha) { 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; } break; } case 22: /* Set mouse cursor */ term_set_user_mouse_cursor(term, string); break; case 30: /* Set tab title */ break; case 52: /* Copy to/from clipboard/primary */ osc_selection(term, string); break; case 66: /* text-size protocol (kitty) */ kitty_text_size(term, string); break; case 99: /* Kitty notifications */ kitty_notification(term, string); break; case 104: { /* Reset Color Number 'c' (whole table if no parameter) */ 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]; term_damage_view(term); } else { for (const char *s_idx = strtok(string, ";"); s_idx != NULL; s_idx = strtok(NULL, ";")) { unsigned idx = 0; for (; *s_idx != '\0'; s_idx++) { char c = *s_idx; idx *= 10; idx += c - '0'; } if (idx >= ALEN(term->colors.table)) { LOG_WARN("invalid OSC 104 color index: %u", idx); continue; } LOG_DBG("resetting color #%u", idx); term->colors.table[idx] = term->conf->colors.table[idx]; term_damage_color(term, COLOR_BASE256, idx); } } break; } case 105: /* Reset Special Color Number 'c' */ break; case 110: /* Reset default text foreground color */ LOG_DBG("resetting foreground color"); term->colors.fg = term->conf->colors.fg; term_damage_color(term, COLOR_DEFAULT, 0); break; case 111: { /* Reset default text background color */ LOG_DBG("resetting background color"); bool alpha_changed = term->colors.alpha != term->conf->colors.alpha; term->colors.bg = term->conf->colors.bg; term->colors.alpha = term->conf->colors.alpha; if (alpha_changed) { wayl_win_alpha_changed(term->window); term_font_subpixel_changed(term); } term_damage_color(term, COLOR_DEFAULT, 0); term_damage_margins(term); break; } case 112: LOG_DBG("resetting cursor color"); term->colors.cursor_fg = term->conf->cursor.color.text; term->colors.cursor_bg = term->conf->cursor.color.cursor; term_damage_cursor(term); break; 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; break; case 133: /* * Shell integration; see * https://iterm2.com/documentation-escape-codes.html (Shell * Integration/FinalTerm) * * [PROMPT]prompt% [COMMAND_START] ls -l * [COMMAND_EXECUTED] * -rw-r--r-- 1 user group 127 May 1 2016 filename * [COMMAND_FINISHED] */ switch (string[0]) { case 'A': LOG_DBG("FTCS_PROMPT: %dx%d", term->grid->cursor.point.row, term->grid->cursor.point.col); term->grid->cur_row->shell_integration.prompt_marker = true; break; case 'B': LOG_DBG("FTCS_COMMAND_START"); break; case 'C': LOG_DBG("FTCS_COMMAND_EXECUTED: %dx%d", term->grid->cursor.point.row, term->grid->cursor.point.col); term->grid->cur_row->shell_integration.cmd_start = term->grid->cursor.point.col; break; case 'D': LOG_DBG("FTCS_COMMAND_FINISHED: %dx%d", term->grid->cursor.point.row, term->grid->cursor.point.col); term->grid->cur_row->shell_integration.cmd_end = term->grid->cursor.point.col; break; } break; case 176: if (string[0] == '?' && string[1] == '\0') { #if 0 /* Disabled for now, see #1894 */ const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; char *reply = xasprintf( "\033]176;%s%s", term->app_id != NULL ? term->app_id : term->conf->app_id, terminator); term_to_slave(term, reply, strlen(reply)); free(reply); #else LOG_WARN("OSC-176 app-id query ignored"); #endif break; } term_set_app_id(term, string); break; case 555: osc_flash(term); break; case 777: { /* * OSC 777 is an URxvt generic escape used to send commands to * perl extensions. The generic syntax is: \E]777;;ST * * We only recognize the 'notify' command, which is, if not * well established, at least fairly well known. */ char *param_brk = strchr(string, ';'); if (param_brk == NULL) { UNHANDLED(); return; } if (strncmp(string, "notify", param_brk - string) == 0) osc_notify(term, param_brk + 1); else UNHANDLED(); break; } default: UNHANDLED(); break; } } bool osc_ensure_size(struct terminal *term, size_t required_size) { if (likely(required_size <= term->vt.osc.size)) return true; const size_t pow2_max = ~(SIZE_MAX >> 1); if (unlikely(required_size > pow2_max)) { LOG_ERR("required OSC buffer size (%zu) exceeds limit (%zu)", required_size, pow2_max); return false; } size_t new_size = max(term->vt.osc.size, 4096); while (new_size < required_size) { new_size <<= 1; } uint8_t *new_data = realloc(term->vt.osc.data, new_size); if (new_data == NULL) { LOG_ERRNO("failed to increase size of OSC buffer"); return false; } LOG_DBG("resized OSC buffer: %zu", new_size); term->vt.osc.data = new_data; term->vt.osc.size = new_size; return true; } foot-1.21.0/osc.h000066400000000000000000000002471476600145200135070ustar00rootroot00000000000000#pragma once #include #include "terminal.h" bool osc_ensure_size(struct terminal *term, size_t required_size); void osc_dispatch(struct terminal *term); foot-1.21.0/pgo/000077500000000000000000000000001476600145200133345ustar00rootroot00000000000000foot-1.21.0/pgo/full-current-session.sh000077500000000000000000000002001476600145200177660ustar00rootroot00000000000000#!/bin/sh set -eux srcdir=$(realpath "${1}") blddir=$(realpath "${2}") "${srcdir}"/pgo/full-inner.sh "${srcdir}" "${blddir}" foot-1.21.0/pgo/full-headless-cage.sh000077500000000000000000000005721476600145200173240ustar00rootroot00000000000000#!/bin/sh set -eux srcdir=$(realpath "${1}") blddir=$(realpath "${2}") runtime_dir=$(mktemp -d) trap "rm -rf '${runtime_dir}'" EXIT INT HUP TERM XDG_RUNTIME_DIR="${runtime_dir}" WLR_RENDERER=pixman WLR_BACKENDS=headless cage "${srcdir}"/pgo/full-inner.sh "${srcdir}" "${blddir}" # Cage's exit code doesn't reflect our script's exit code [ -f "${blddir}"/pgo-ok ] || exit 1 foot-1.21.0/pgo/full-headless-sway-inner.sh000077500000000000000000000002141476600145200205120ustar00rootroot00000000000000#!/bin/sh set -ux srcdir=$(realpath "${1}") blddir=$(realpath "${2}") "${srcdir}"/pgo/full-inner.sh "${srcdir}" "${blddir}" swaymsg exit foot-1.21.0/pgo/full-headless-sway.sh000077500000000000000000000012311476600145200174010ustar00rootroot00000000000000#!/bin/sh set -eux srcdir=$(realpath "${1}") blddir=$(realpath "${2}") runtime_dir=$(mktemp -d) sway_conf=$(mktemp) cleanup() { rm -f "${sway_conf}" rm -rf "${runtime_dir}" } trap cleanup EXIT INT HUP TERM # Generate a custom config that executes our generate-pgo-data script > "${sway_conf}" echo "exec '${srcdir}'/pgo/full-headless-sway-inner.sh '${srcdir}' '${blddir}'" # Run Sway. full-headless-sway-inner.sh ends with a 'swaymsg exit' XDG_RUNTIME_DIR="${runtime_dir}" WLR_RENDERER=pixman WLR_BACKENDS=headless sway -c "${sway_conf}" --unsupported-gpu # Sway's exit code doesn't reflect our script's exit code [ -f "${blddir}"/pgo-ok ] || exit 1 foot-1.21.0/pgo/full-inner.sh000077500000000000000000000012341476600145200157460ustar00rootroot00000000000000#!/bin/sh set -eux srcdir=$(realpath "${1}") blddir=$(realpath "${2}") . "${srcdir}"/pgo/options pgo_data=$(mktemp) trap "rm -f '${pgo_data}'" EXIT INT HUP TERM rm -f "${blddir}"/pgo-ok # To ensure profiling data is generated in the build directory cd "${blddir}" "${blddir}"/utils/xtgettcap "${blddir}"/footclient --version "${blddir}"/foot \ --config=/dev/null \ --override tweak.grapheme-shaping=no \ --term=xterm \ sh -c " set -eux '${srcdir}/scripts/generate-alt-random-writes.py' \ ${script_options} \"${pgo_data}\" cat \"${pgo_data}\" " touch "${blddir}"/pgo-ok foot-1.21.0/pgo/options000066400000000000000000000002301476600145200147450ustar00rootroot00000000000000script_options="--scroll --scroll-region --colors-regular --colors-bright --colors-256 --colors-rgb --attr-bold --attr-italic --attr-underline --sixel" foot-1.21.0/pgo/partial.sh000077500000000000000000000010711476600145200153260ustar00rootroot00000000000000#!/bin/sh set -eux srcdir=$(realpath "${1}") blddir=$(realpath "${2}") . "${srcdir}"/pgo/options pgo_data=$(mktemp) trap "rm -f ${pgo_data}" EXIT INT HUP TERM rm -f "${blddir}"/pgo-ok "${srcdir}"/scripts/generate-alt-random-writes.py \ --rows=67 \ --cols=135 \ ${script_options} \ "${pgo_data}" # To ensure profiling data is generated in the build directory cd "${blddir}" "${blddir}"/utils/xtgettcap "${blddir}"/footclient --version "${blddir}"/foot --version "${blddir}"/pgo "${pgo_data}" touch "${blddir}"/pgo-ok foot-1.21.0/pgo/pgo.c000066400000000000000000000244051476600145200142720ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include "async.h" #include "config.h" #include "key-binding.h" #include "reaper.h" #include "sixel.h" #include "user-notification.h" #include "vt.h" extern bool fdm_ptmx(struct fdm *fdm, int fd, int events, void *data); static void usage(const char *prog_name) { printf( "Usage: %s stimuli-file1 stimuli-file2 ... stimuli-fileN\n", prog_name); } enum async_write_status async_write(int fd, const void *data, size_t len, size_t *idx) { return ASYNC_WRITE_DONE; } bool fdm_add(struct fdm *fdm, int fd, int events, fdm_fd_handler_t handler, void *data) { return true; } bool fdm_del(struct fdm *fdm, int fd) { return true; } bool fdm_event_add(struct fdm *fdm, int fd, int events) { return true; } bool fdm_event_del(struct fdm *fdm, int fd, int events) { return true; } bool render_resize( struct terminal *term, int width, int height, uint8_t resize_options) { return true; } void render_refresh(struct terminal *term) {} void render_refresh_csd(struct terminal *term) {} void render_refresh_title(struct terminal *term) {} void render_refresh_app_id(struct terminal *term) {} void render_refresh_icon(struct terminal *term) {} void render_overlay(struct terminal *term) {} bool render_xcursor_is_valid(const struct seat *seat, const char *cursor) { return true; } bool render_xcursor_set(struct seat *seat, struct terminal *term, enum cursor_shape shape) { return true; } enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y) { return CURSOR_SHAPE_LEFT_PTR; } struct wl_window * wayl_win_init(struct terminal *term, const char *token) { return NULL; } void wayl_win_destroy(struct wl_window *win) {} void wayl_win_alpha_changed(struct wl_window *win) {} bool wayl_win_set_urgent(struct wl_window *win) { return true; } bool wayl_win_ring_bell(const struct wl_window *win) { return true; } bool wayl_fractional_scaling(const struct wayland *wayl) { return true; } pid_t spawn(struct reaper *reaper, const char *cwd, char *const argv[], int stdin_fd, int stdout_fd, int stderr_fd, reaper_cb cb, void *cb_data, const char *xdg_activation_token) { return 2; } pid_t slave_spawn( int ptmx, int argc, const char *cwd, char *const *argv, char *const *envp, const env_var_list_t *extra_env_vars, const char *term_env, const char *conf_shell, bool login_shell, const user_notifications_t *notifications) { return 0; } int render_worker_thread(void *_ctx) { return 0; } bool render_do_linear_blending(const struct terminal *term) { return false; } struct extraction_context * extract_begin(enum selection_kind kind, bool strip_trailing_empty) { return NULL; } bool extract_one( const struct terminal *term, const struct row *row, const struct cell *cell, int col, void *context) { return true; } bool extract_finish(struct extraction_context *context, char **text, size_t *len) { return true; } void cmd_scrollback_up(struct terminal *term, int rows) {} void cmd_scrollback_down(struct terminal *term, int rows) {} void ime_enable(struct seat *seat) {} void ime_disable(struct seat *seat) {} void ime_reset_preedit(struct seat *seat) {} bool notify_notify(struct terminal *term, struct notification *notif) { return true; } void notify_close(struct terminal *term, const char *id) { } void notify_free(struct terminal *term, struct notification *notif) { } void notify_icon_add(struct terminal *term, const char *id, const char *symbolic_name, const uint8_t *data, size_t data_sz) { } void notify_icon_del(struct terminal *term, const char *id) { } void notify_icon_free(struct notification_icon *icon) { } void reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data) {} void reaper_del(struct reaper *reaper, pid_t pid) {} void urls_reset(struct terminal *term) {} void shm_unref(struct buffer *buf) {} void shm_chain_free(struct buffer_chain *chain) {} struct buffer_chain * shm_chain_new( struct wayland *wayl, bool scrollable, size_t pix_instances, bool ten_bit_it_if_capable) { return NULL; } void search_selection_cancelled(struct terminal *term) {} void get_current_modifiers(const struct seat *seat, xkb_mod_mask_t *effective, xkb_mod_mask_t *consumed, uint32_t key, bool filter_locked) {} static struct key_binding_set kbd; static bool kbd_initialized = false; struct key_binding_set * key_binding_for( struct key_binding_manager *mgr, const struct config *conf, const struct seat *seat) { return &kbd; } void key_binding_new_for_conf( struct key_binding_manager *mgr, const struct wayland *wayl, const struct config *conf) { if (!kbd_initialized) { kbd_initialized = true; kbd = (struct key_binding_set){ .key = tll_init(), .search = tll_init(), .url = tll_init(), .mouse = tll_init(), .selection_overrides = 0, }; } } void key_binding_unref(struct key_binding_manager *mgr, const struct config *conf) { } int main(int argc, const char *const *argv) { if (argc < 2) { usage(argv[0]); return EXIT_FAILURE; } const int row_count = 67; const int col_count = 135; const int grid_row_count = 16384; int lower_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (lower_fd < 0) return EXIT_FAILURE; int upper_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (upper_fd < 0) { close(lower_fd); return EXIT_FAILURE; } struct row **normal_rows = calloc(grid_row_count, sizeof(normal_rows[0])); struct row **alt_rows = calloc(grid_row_count, sizeof(alt_rows[0])); for (int i = 0; i < grid_row_count; i++) { normal_rows[i] = calloc(1, sizeof(*normal_rows[i])); normal_rows[i]->cells = calloc(col_count, sizeof(normal_rows[i]->cells[0])); alt_rows[i] = calloc(1, sizeof(*alt_rows[i])); alt_rows[i]->cells = calloc(col_count, sizeof(alt_rows[i]->cells[0])); } struct config conf = { .tweak = { .delayed_render_lower_ns = 500000, /* 0.5ms */ .delayed_render_upper_ns = 16666666 / 2, /* half a frame period (60Hz) */ }, }; struct wayland wayl = { .seats = tll_init(), .monitors = tll_init(), .terms = tll_init(), }; struct terminal term = { .conf = &conf, .wl = &wayl, .grid = &term.normal, .normal = { .num_rows = grid_row_count, .num_cols = col_count, .rows = normal_rows, .cur_row = normal_rows[0], }, .alt = { .num_rows = grid_row_count, .num_cols = col_count, .rows = alt_rows, .cur_row = alt_rows[0], }, .scale = 1, .width = col_count * 8, .height = row_count * 15, .cols = col_count, .rows = row_count, .cell_width = 8, .cell_height = 15, .scroll_region = { .start = 0, .end = row_count, }, .selection = { .coords = { .start = {-1, -1}, .end = {-1, -1}, }, }, .delayed_render_timer = { .lower_fd = lower_fd, .upper_fd = upper_fd }, .sixel = { .palette_size = SIXEL_MAX_COLORS, .max_width = SIXEL_MAX_WIDTH, .max_height = SIXEL_MAX_HEIGHT, }, }; tll_push_back(wayl.terms, &term); int ret = EXIT_FAILURE; for (int i = 1; i < argc; i++) { struct stat st; if (stat(argv[i], &st) < 0) { fprintf(stderr, "error: %s: failed to stat: %s\n", argv[i], strerror(errno)); goto out; } uint8_t *data = malloc(st.st_size); if (data == NULL) { fprintf(stderr, "error: %s: failed to allocate buffer: %s\n", argv[i], strerror(errno)); goto out; } int fd = open(argv[1], O_RDONLY); if (fd < 0) { fprintf(stderr, "error: %s: failed to open: %s\n", argv[i], strerror(errno)); goto out; } ssize_t amount = read(fd, data, st.st_size); if (amount != st.st_size) { fprintf(stderr, "error: %s: failed to read: %s\n", argv[i], strerror(errno)); goto out; } close(fd); #if defined(MEMFD_CREATE) int mem_fd = memfd_create("foot-pgo-ptmx", MFD_CLOEXEC); #elif defined(__FreeBSD__) // memfd_create on FreeBSD 13 is SHM_ANON without sealing support int mem_fd = shm_open(SHM_ANON, O_RDWR | O_CLOEXEC, 0600); #else char name[] = "/tmp/foot-pgo-ptmx-XXXXXX"; int mem_fd = mkostemp(name, O_CLOEXEC); unlink(name); #endif if (mem_fd < 0) { fprintf(stderr, "error: failed to create memory FD\n"); goto out; } if (write(mem_fd, data, st.st_size) < 0) { fprintf(stderr, "error: failed to write memory FD\n"); close(mem_fd); goto out; } free(data); term.ptmx = mem_fd; lseek(mem_fd, 0, SEEK_SET); printf("Feeding VT parser with %s (%lld bytes)\n", argv[i], (long long)st.st_size); while (lseek(mem_fd, 0, SEEK_CUR) < st.st_size) { if (!fdm_ptmx(NULL, -1, EPOLLIN, &term)) { fprintf(stderr, "error: fdm_ptmx() failed\n"); close(mem_fd); goto out; } } close(mem_fd); } ret = EXIT_SUCCESS; out: tll_free(wayl.terms); for (int i = 0; i < grid_row_count; i++) { if (normal_rows[i] != NULL) free(normal_rows[i]->cells); free(normal_rows[i]); if (alt_rows[i] != NULL) free(alt_rows[i]->cells); free(alt_rows[i]); } free(normal_rows); free(alt_rows); close(lower_fd); close(upper_fd); return ret; } foot-1.21.0/pgo/pgo.sh000077500000000000000000000050201476600145200144550ustar00rootroot00000000000000#!/bin/sh set -eu usage_and_die() { echo "Usage: ${0} none|partial|full-current-session|full-headless-sway|full-headless-cage|[auto] [meson options]" exit 1 } [ ${#} -ge 3 ] || usage_and_die mode=${1} srcdir=$(realpath "${2}") blddir=$(realpath "${3}") shift 3 # if [ -e "${blddir}" ]; then # echo "error: ${blddir}: build directory already exists" # exit 1 # fi if [ ! -f "${srcdir}"/generate-version.sh ]; then echo "error: ${srcdir}: does not appear to be a foot source directory" exit 1 fi compiler=other do_pgo=no CFLAGS="${CFLAGS-} -O3" case $(${CC-cc} --version) in *Free\ Software\ Foundation*) compiler=gcc do_pgo=yes ;; *clang*) compiler=clang if command -v llvm-profdata > /dev/null; then do_pgo=yes CFLAGS="${CFLAGS} -Wno-ignored-optimization-argument" fi ;; esac case ${mode} in partial|full-current-session|full-headless-sway|full-headless-cage) ;; none) do_pgo=no ;; auto) if [ -n "${WAYLAND_DISPLAY+x}" ]; then mode=full-current-session elif command -v sway > /dev/null; then mode=full-headless-sway elif command -v cage > /dev/null; then mode=full-headless-cage else mode=partial fi ;; *) usage_and_die ;; esac set -x # echo "source: ${srcdir}" # echo "build: ${blddir}" # echo "compiler: ${compiler}" # echo "mode: ${mode}" # echo "CFLAGS: ${CFLAGS}" export CFLAGS export CCACHE_DISABLE=1 meson setup --buildtype=release -Db_lto=true "${@}" "${blddir}" "${srcdir}" if [ ${do_pgo} = yes ]; then find "${blddir}" \ '(' \ -name "*.gcda" -o \ -name "*.profraw" -o \ -name default.profdata \ ')' \ -delete meson configure "${blddir}" -Db_pgo=generate ninja -C "${blddir}" # If fcft/tllist are subprojects, we need to ensure their tests # have been executed, or we'll get "profile count data file not # found" errors. ninja -C "${blddir}" test # Run mode-dependent script to generate profiling data export LLVM_PROFILE_FILE="${blddir}/default_%m.profraw" "${srcdir}"/pgo/${mode}.sh "${srcdir}" "${blddir}" if [ ${compiler} = clang ]; then llvm-profdata \ merge \ "${blddir}"/default_*.profraw \ --output="${blddir}"/default.profdata fi meson configure "${blddir}" -Db_pgo=use fi ninja -C "${blddir}" foot-1.21.0/quirks.c000066400000000000000000000036341476600145200142370ustar00rootroot00000000000000#include "quirks.h" #include #include #include #define LOG_MODULE "quirks" #define LOG_ENABLE_DBG 0 #include "log.h" #include "util.h" static bool is_weston(void) { static bool is_weston = false; static bool initialized = false; if (!initialized) { initialized = true; is_weston = getenv("WESTON_CONFIG_FILE") != NULL; if (is_weston) LOG_WARN("applying wl_subsurface_set_desync() workaround for weston"); } return is_weston; } void quirk_weston_subsurface_desync_on(struct wl_subsurface *sub) { if (!is_weston()) return; wl_subsurface_set_desync(sub); } void quirk_weston_subsurface_desync_off(struct wl_subsurface *sub) { if (!is_weston()) return; wl_subsurface_set_sync(sub); } void quirk_weston_csd_on(struct terminal *term) { if (term->window->csd_mode != CSD_YES) return; if (term->window->is_fullscreen) return; for (int i = 0; i < ALEN(term->window->csd.surface); i++) quirk_weston_subsurface_desync_on(term->window->csd.surface[i].sub); } void quirk_weston_csd_off(struct terminal *term) { if (term->window->csd_mode != CSD_YES) return; if (term->window->is_fullscreen) return; for (int i = 0; i < ALEN(term->window->csd.surface); i++) quirk_weston_subsurface_desync_off(term->window->csd.surface[i].sub); } 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; } void quirk_sway_subsurface_unmap(struct terminal *term) { return; if (!is_sway()) return; wl_surface_damage_buffer(term->window->surface.surf, 0, 0, INT32_MAX, INT32_MAX); } foot-1.21.0/quirks.h000066400000000000000000000015211476600145200142350ustar00rootroot00000000000000#pragma once #include #include "terminal.h" /* * On weston (8.0), synchronized subsurfaces aren't updated correctly. * They appear to render once, but after that, updates are * sporadic. Sometimes they update, most of the time they don't. * * Adding explicit parent surface commits right after the subsurface * commit doesn't help (and would be useless anyway, since it would * defeat the purpose of having the subsurface synchronized in the * first place). */ void quirk_weston_subsurface_desync_on(struct wl_subsurface *sub); void quirk_weston_subsurface_desync_off(struct wl_subsurface *sub); /* Shortcuts to call desync_{on,off} on all CSD subsurfaces */ void quirk_weston_csd_on(struct terminal *term); void quirk_weston_csd_off(struct terminal *term); void quirk_sway_subsurface_unmap(struct terminal *term); foot-1.21.0/reaper.c000066400000000000000000000047701476600145200142010ustar00rootroot00000000000000#include "reaper.h" #include #include #include #include #include #include #define LOG_MODULE "reaper" #define LOG_ENABLE_DBG 0 #include "log.h" struct child { pid_t pid; reaper_cb cb; void *cb_data; }; struct reaper { struct fdm *fdm; tll(struct child) children; }; static bool fdm_reap(struct fdm *fdm, int signo, void *data); struct reaper * reaper_init(struct fdm *fdm) { struct reaper *reaper = malloc(sizeof(*reaper)); if (unlikely(reaper == NULL)) { LOG_ERRNO("malloc() failed"); return NULL; } *reaper = (struct reaper){ .fdm = fdm, .children = tll_init(), }; if (!fdm_signal_add(fdm, SIGCHLD, &fdm_reap, reaper)) goto err; return reaper; err: tll_free(reaper->children); free(reaper); return NULL; } void reaper_destroy(struct reaper *reaper) { if (reaper == NULL) return; fdm_signal_del(reaper->fdm, SIGCHLD); tll_free(reaper->children); free(reaper); } void reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data) { LOG_DBG("adding pid=%d", pid); tll_push_back( reaper->children, ((struct child){.pid = pid, .cb = cb, .cb_data = cb_data})); } void reaper_del(struct reaper *reaper, pid_t pid) { tll_foreach(reaper->children, it) { if (it->item.pid == pid) { tll_remove(reaper->children, it); break; } } } static bool fdm_reap(struct fdm *fdm, int signo, void *data) { struct reaper *reaper = data; while (true) { int status; pid_t pid = waitpid(-1, &status, WNOHANG); if (pid <= 0) break; if (WIFEXITED(status)) LOG_DBG("pid=%d: exited with status=%d", pid, WEXITSTATUS(status)); else if (WIFSIGNALED(status)) LOG_DBG("pid=%d: killed by signal=%d", pid, WTERMSIG(status)); else LOG_DBG("pid=%d: died of unknown resason", pid); tll_foreach(reaper->children, it) { struct child *_child = &it->item; if (_child->pid != pid) continue; /* Make sure we remove it *before* the callback, since it too * may remove it */ struct child child = it->item; tll_remove(reaper->children, it); if (child.cb != NULL) child.cb(reaper, child.pid, status, child.cb_data); break; } } return true; } foot-1.21.0/reaper.h000066400000000000000000000006241476600145200142000ustar00rootroot00000000000000#pragma once #include #include #include "fdm.h" struct reaper; struct reaper *reaper_init(struct fdm *fdm); void reaper_destroy(struct reaper *reaper); typedef void (*reaper_cb)( struct reaper *reaper, pid_t pid, int status, void *data); void reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data); void reaper_del(struct reaper *reaper, pid_t pid); foot-1.21.0/render.c000066400000000000000000005316771476600145200142150ustar00rootroot00000000000000#include "render.h" #include #include #include #include #include #include #include #include #include #include "macros.h" #if HAS_INCLUDE() #include #define pthread_setname_np(thread, name) (pthread_set_name_np(thread, name), 0) #elif defined(__NetBSD__) #define pthread_setname_np(thread, name) pthread_setname_np(thread, "%s", (void *)name) #endif #include #include #include #include #include #define LOG_MODULE "render" #define LOG_ENABLE_DBG 0 #include "log.h" #include "box-drawing.h" #include "char32.h" #include "config.h" #include "cursor-shape.h" #include "grid.h" #include "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" #define TIME_SCROLL_DAMAGE 0 struct renderer { struct fdm *fdm; struct wayland *wayl; }; static struct { size_t total; size_t zero; /* commits presented in less than one frame interval */ size_t one; /* commits presented in one frame interval */ size_t two; /* commits presented in two or more frame intervals */ } presentation_statistics = {0}; static void fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data); struct renderer * render_init(struct fdm *fdm, struct wayland *wayl) { struct renderer *renderer = malloc(sizeof(*renderer)); if (unlikely(renderer == NULL)) { LOG_ERRNO("malloc() failed"); return NULL; } *renderer = (struct renderer) { .fdm = fdm, .wayl = wayl, }; if (!fdm_hook_add(fdm, &fdm_hook_refresh_pending_terminals, renderer, FDM_HOOK_PRIORITY_NORMAL)) { LOG_ERR("failed to register FDM hook"); free(renderer); return NULL; } return renderer; } void render_destroy(struct renderer *renderer) { if (renderer == NULL) return; fdm_hook_del(renderer->fdm, &fdm_hook_refresh_pending_terminals, FDM_HOOK_PRIORITY_NORMAL); free(renderer); } static void DESTRUCTOR log_presentation_statistics(void) { if (presentation_statistics.total == 0) return; const size_t total = presentation_statistics.total; LOG_INFO("presentation statistics: zero=%f%%, one=%f%%, two=%f%%", 100. * presentation_statistics.zero / total, 100. * presentation_statistics.one / total, 100. * presentation_statistics.two / total); } static void sync_output(void *data, struct wp_presentation_feedback *wp_presentation_feedback, struct wl_output *output) { } struct presentation_context { struct terminal *term; struct timeval input; struct timeval commit; }; static void presented(void *data, struct wp_presentation_feedback *wp_presentation_feedback, uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec, uint32_t refresh, uint32_t seq_hi, uint32_t seq_lo, uint32_t flags) { struct presentation_context *ctx = data; struct terminal *term = ctx->term; const struct timeval *input = &ctx->input; const struct timeval *commit = &ctx->commit; const struct timeval presented = { .tv_sec = (uint64_t)tv_sec_hi << 32 | tv_sec_lo, .tv_usec = tv_nsec / 1000, }; bool use_input = (input->tv_sec > 0 || input->tv_usec > 0) && timercmp(&presented, input, >); char msg[1024]; int chars = 0; if (use_input && timercmp(&presented, input, <)) return; else if (timercmp(&presented, commit, <)) return; LOG_DBG("commit: %lu s %lu µs, presented: %lu s %lu µs", commit->tv_sec, commit->tv_usec, presented.tv_sec, presented.tv_usec); if (use_input) { struct timeval diff; timersub(commit, input, &diff); chars += snprintf( &msg[chars], sizeof(msg) - chars, "input - %llu µs -> ", (unsigned long long)diff.tv_usec); } struct timeval diff; timersub(&presented, commit, &diff); chars += snprintf( &msg[chars], sizeof(msg) - chars, "commit - %llu µs -> ", (unsigned long long)diff.tv_usec); if (use_input) { xassert(timercmp(&presented, input, >)); timersub(&presented, input, &diff); } else { xassert(timercmp(&presented, commit, >)); timersub(&presented, commit, &diff); } chars += snprintf( &msg[chars], sizeof(msg) - chars, "presented (total: %llu µs)", (unsigned long long)diff.tv_usec); unsigned frame_count = 0; if (tll_length(term->window->on_outputs) > 0) { const struct monitor *mon = tll_front(term->window->on_outputs); frame_count = (diff.tv_sec * 1000000. + diff.tv_usec) / (1000000. / mon->refresh); } presentation_statistics.total++; if (frame_count >= 2) presentation_statistics.two++; else if (frame_count >= 1) presentation_statistics.one++; else presentation_statistics.zero++; #define _log_fmt "%s (more than %u frames)" if (frame_count >= 2) LOG_ERR(_log_fmt, msg, frame_count); else if (frame_count >= 1) LOG_WARN(_log_fmt, msg, frame_count); else LOG_INFO(_log_fmt, msg, frame_count); #undef _log_fmt wp_presentation_feedback_destroy(wp_presentation_feedback); free(ctx); } static void discarded(void *data, struct wp_presentation_feedback *wp_presentation_feedback) { struct presentation_context *ctx = data; wp_presentation_feedback_destroy(wp_presentation_feedback); free(ctx); } static const struct wp_presentation_feedback_listener presentation_feedback_listener = { .sync_output = &sync_output, .presented = &presented, .discarded = &discarded, }; static struct fcft_font * attrs_to_font(const struct terminal *term, const struct attributes *attrs) { int idx = attrs->italic << 1 | attrs->bold; return term->fonts[idx]; } static pixman_color_t color_hex_to_pixman_srgb(uint32_t color, uint16_t alpha) { return (pixman_color_t){ .alpha = alpha, /* Consider alpha linear already? */ .red = srgb_decode_8_to_16((color >> 16) & 0xff), .green = srgb_decode_8_to_16((color >> 8) & 0xff), .blue = srgb_decode_8_to_16((color >> 0) & 0xff), }; } static inline pixman_color_t color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha, bool srgb) { pixman_color_t ret; if (srgb) ret = color_hex_to_pixman_srgb(color, alpha); else { ret = (pixman_color_t){ .red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)), .green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)), .blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)), .alpha = alpha, }; } ret.red = (uint32_t)ret.red * alpha / 0xffff; ret.green = (uint32_t)ret.green * alpha / 0xffff; ret.blue = (uint32_t)ret.blue * alpha / 0xffff; return ret; } static inline pixman_color_t color_hex_to_pixman(uint32_t color, bool srgb) { /* Count on the compiler optimizing this */ return color_hex_to_pixman_with_alpha(color, 0xffff, srgb); } static inline uint32_t color_decrease_luminance(uint32_t color) { 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); } 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; if (likely(custom_dim == 0)) return color_decrease_luminance(color); 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.dim[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); } static inline uint32_t color_brighten(const struct terminal *term, uint32_t color) { /* * First try to match the color against the base 8 colors. If we * find a match, return the corresponding bright color. */ if (term->conf->bold_in_bright.palette_based) { for (size_t i = 0; i < 8; i++) { if (term->colors.table[i] == color) return term->colors.table[i + 8]; } return color; } int hue, sat, lum; rgb_to_hsl(color, &hue, &sat, &lum); lum = (int)roundf(lum * term->conf->bold_in_bright.amount); return hsl_to_rgb(hue, sat, min(lum, 100)); } static void draw_hollow_block(const struct terminal *term, pixman_image_t *pix, const pixman_color_t *color, int x, int y, int cell_cols) { const int scale = (int)roundf(term->scale); const int width = min(min(scale, term->cell_width), term->cell_height); pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 4, (pixman_rectangle16_t []){ {x, y, cell_cols * term->cell_width, width}, /* top */ {x, y, width, term->cell_height}, /* left */ {x + cell_cols * term->cell_width - width, y, width, term->cell_height}, /* right */ {x, y + term->cell_height - width, cell_cols * term->cell_width, width}, /* bottom */ }); } static void draw_beam_cursor(const struct terminal *term, pixman_image_t *pix, const struct fcft_font *font, const pixman_color_t *color, int x, int y) { int baseline = y + term->font_baseline - term->fonts[0]->ascent; pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){ x, baseline, term_pt_or_px_as_pixels(term, &term->conf->cursor.beam_thickness), term->fonts[0]->ascent + term->fonts[0]->descent}); } static int underline_offset(const struct terminal *term, const struct fcft_font *font) { return term->font_baseline - (term->conf->use_custom_underline_offset ? -term_pt_or_px_as_pixels(term, &term->conf->underline_offset) : font->underline.position); } static void draw_underline_cursor(const struct terminal *term, pixman_image_t *pix, const struct fcft_font *font, const pixman_color_t *color, int x, int y, int cols) { int thickness = term->conf->cursor.underline_thickness.px >= 0 ? term_pt_or_px_as_pixels( term, &term->conf->cursor.underline_thickness) : font->underline.thickness; /* Make sure the line isn't positioned below the cell */ const int y_ofs = min(underline_offset(term, font) + thickness, term->cell_height - thickness); pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){ x, y + y_ofs, cols * term->cell_width, thickness}); } static void draw_underline(const struct terminal *term, pixman_image_t *pix, const struct fcft_font *font, const pixman_color_t *color, int x, int y, int cols) { const int thickness = term->conf->underline_thickness.px >= 0 ? term_pt_or_px_as_pixels( term, &term->conf->underline_thickness) : font->underline.thickness; /* Make sure the line isn't positioned below the cell */ const int y_ofs = min(underline_offset(term, font), term->cell_height - thickness); pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){ x, y + y_ofs, cols * term->cell_width, thickness}); } static void draw_styled_underline(const struct terminal *term, pixman_image_t *pix, const struct fcft_font *font, const pixman_color_t *color, enum underline_style style, int x, int y, int cols) { xassert(style != UNDERLINE_NONE); if (style == UNDERLINE_SINGLE) { draw_underline(term, pix, font, color, x, y, cols); return; } const int thickness = term->conf->underline_thickness.px >= 0 ? term_pt_or_px_as_pixels( term, &term->conf->underline_thickness) : font->underline.thickness; int y_ofs; /* Make sure the line isn't positioned below the cell */ switch (style) { case UNDERLINE_DOUBLE: case UNDERLINE_CURLY: y_ofs = min(underline_offset(term, font), term->cell_height - thickness * 3); break; case UNDERLINE_DASHED: case UNDERLINE_DOTTED: y_ofs = min(underline_offset(term, font), term->cell_height - thickness); break; case UNDERLINE_NONE: case UNDERLINE_SINGLE: default: BUG("unexpected underline style: %d", (int)style); return; } const int ceil_w = cols * term->cell_width; switch (style) { case UNDERLINE_DOUBLE: { const pixman_rectangle16_t rects[] = { {x, y + y_ofs, ceil_w, thickness}, {x, y + y_ofs + thickness * 2, ceil_w, thickness}}; pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 2, rects); break; } case UNDERLINE_DASHED: { const int ceil_w = cols * term->cell_width; const int dash_w = ceil_w / 3 + (ceil_w % 3 > 0); const pixman_rectangle16_t rects[] = { {x, y + y_ofs, dash_w, thickness}, {x + dash_w * 2, y + y_ofs, dash_w, thickness}, }; pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 2, rects); break; } case UNDERLINE_DOTTED: { /* Number of dots per cell */ int per_cell = (term->cell_width / thickness) / 2; if (per_cell == 0) per_cell = 1; xassert(per_cell >= 1); /* Spacing between dots; start with the same width as the dots themselves, then widen them if necessary, to consume unused pixels */ int spacing[per_cell]; for (int i = 0; i < per_cell; i++) spacing[i] = thickness; /* Pixels remaining at the end of the cell */ int remaining = term->cell_width - (per_cell * 2) * thickness; /* Spread out the left-over pixels across the spacing between the dots */ for (int i = 0; remaining > 0; i = (i + 1) % per_cell, remaining--) spacing[i]++; xassert(remaining <= 0); pixman_rectangle16_t rects[per_cell]; int dot_x = x; for (int i = 0; i < per_cell; i++) { rects[i] = (pixman_rectangle16_t){ dot_x, y + y_ofs, thickness, thickness }; dot_x += thickness + spacing[i]; } pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, per_cell, rects); break; } case UNDERLINE_CURLY: { const int top = y + y_ofs; const int bot = top + thickness * 3; const int half_x = x + ceil_w / 2.0, full_x = x + ceil_w; const double bt_2 = (bot - top) * (bot - top); const double th_2 = thickness * thickness; const double hx_2 = ceil_w * ceil_w / 4.0; const int th = round(sqrt(th_2 + (th_2 * bt_2 / hx_2)) / 2.); #define I(x) pixman_int_to_fixed(x) const pixman_trapezoid_t traps[] = { #if 0 /* characters sit within the "dips" of the curlies */ { I(top), I(bot), {{I(x), I(top + th)}, {I(half_x), I(bot + th)}}, {{I(x), I(top - th)}, {I(half_x), I(bot - th)}}, }, { I(top), I(bot), {{I(half_x), I(bot - th)}, {I(full_x), I(top - th)}}, {{I(half_x), I(bot + th)}, {I(full_x), I(top + th)}}, } #else /* characters sit on top of the curlies */ { I(top), I(bot), {{I(x), I(bot - th)}, {I(half_x), I(top - th)}}, {{I(x), I(bot + th)}, {I(half_x), I(top + th)}}, }, { I(top), I(bot), {{I(half_x), I(top + th)}, {I(full_x), I(bot + th)}}, {{I(half_x), I(top - th)}, {I(full_x), I(bot - th)}}, } #endif }; pixman_image_t *fill = pixman_image_create_solid_fill(color); pixman_composite_trapezoids( PIXMAN_OP_OVER, fill, pix, PIXMAN_a8, 0, 0, 0, 0, sizeof(traps) / sizeof(traps[0]), traps); pixman_image_unref(fill); break; } case UNDERLINE_NONE: case UNDERLINE_SINGLE: BUG("underline styles not supposed to be handled here"); break; } } static void draw_strikeout(const struct terminal *term, pixman_image_t *pix, const struct fcft_font *font, const pixman_color_t *color, int x, int y, int cols) { const int thickness = term->conf->strikeout_thickness.px >= 0 ? term_pt_or_px_as_pixels( term, &term->conf->strikeout_thickness) : font->strikeout.thickness; /* Try to center custom strikeout */ const int position = term->conf->strikeout_thickness.px >= 0 ? font->strikeout.position - round(font->strikeout.thickness / 2.) + round(thickness / 2.) : font->strikeout.position; pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){ x, y + term->font_baseline - position, cols * term->cell_width, thickness}); } static void cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, const pixman_color_t *fg, const pixman_color_t *bg, pixman_color_t *cursor_color, pixman_color_t *text_color, bool gamma_correct) { if (term->colors.cursor_bg >> 31) *cursor_color = color_hex_to_pixman(term->colors.cursor_bg, gamma_correct); else *cursor_color = *fg; if (term->colors.cursor_fg >> 31) *text_color = color_hex_to_pixman(term->colors.cursor_fg, gamma_correct); else { *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, gamma_correct); } } if (text_color->red == cursor_color->red && text_color->green == cursor_color->green && text_color->blue == cursor_color->blue) { *text_color = color_hex_to_pixman(term->colors.bg, gamma_correct); *cursor_color = color_hex_to_pixman(term->colors.fg, gamma_correct); } } static void draw_cursor(const struct terminal *term, const struct cell *cell, const struct fcft_font *font, pixman_image_t *pix, pixman_color_t *fg, const pixman_color_t *bg, int x, int y, int cols) { pixman_color_t cursor_color; pixman_color_t text_color; cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color, render_do_linear_blending(term)); if (unlikely(!term->kbd_focus)) { switch (term->conf->cursor.unfocused_style) { case CURSOR_UNFOCUSED_UNCHANGED: break; case CURSOR_UNFOCUSED_HOLLOW: draw_hollow_block(term, pix, &cursor_color, x, y, cols); return; case CURSOR_UNFOCUSED_NONE: return; } } switch (term->cursor_style) { case CURSOR_BLOCK: if (likely(term->cursor_blink.state == CURSOR_BLINK_ON) || !term->kbd_focus) { *fg = text_color; pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, &cursor_color, 1, &(pixman_rectangle16_t){x, y, cols * term->cell_width, term->cell_height}); } break; case CURSOR_BEAM: if (likely(term->cursor_blink.state == CURSOR_BLINK_ON || !term->kbd_focus)) { draw_beam_cursor(term, pix, font, &cursor_color, x, y); } break; case CURSOR_UNDERLINE: if (likely(term->cursor_blink.state == CURSOR_BLINK_ON || !term->kbd_focus)) { draw_underline_cursor(term, pix, font, &cursor_color, x, y, cols); } break; case CURSOR_HOLLOW: if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) draw_hollow_block(term, pix, &cursor_color, x, y, cols); break; } } static int render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damage, struct row *row, int row_no, int col, bool has_cursor) { struct cell *cell = &row->cells[col]; if (cell->attrs.clean) return 0; cell->attrs.clean = 1; cell->attrs.confined = true; int width = term->cell_width; int height = term->cell_height; const int x = term->margins.left + col * width; const int y = term->margins.top + row_no * height; bool is_selected = cell->attrs.selected; uint32_t _fg = 0; uint32_t _bg = 0; uint16_t 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) { uint32_t swap = _fg; _fg = _bg; _bg = swap; } else if (cell->attrs.bg_src == COLOR_DEFAULT) { if (term->window->is_fullscreen) { /* * 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); } else { alpha = term->colors.alpha; } } } if (unlikely(is_selected && _fg == _bg)) { /* Invert bg when selected/highlighted text has same fg/bg */ _bg = ~_bg; alpha = 0xffff; } if (cell->attrs.dim) _fg = color_dim(term, _fg); if (term->conf->bold_in_bright.enabled && cell->attrs.bold) _fg = color_brighten(term, _fg); if (cell->attrs.blink && term->blink.state == BLINK_OFF) _fg = color_decrease_luminance(_fg); const bool gamma_correct = render_do_linear_blending(term); pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); struct fcft_font *font = attrs_to_font(term, &cell->attrs); const struct composed *composed = NULL; const struct fcft_grapheme *grapheme = NULL; const struct fcft_glyph *single = NULL; const struct fcft_glyph **glyphs = NULL; unsigned glyph_count = 0; char32_t base = cell->wc; int cell_cols = 1; if (base != 0) { if (unlikely( /* Classic box drawings */ (base >= GLYPH_BOX_DRAWING_FIRST && base <= GLYPH_BOX_DRAWING_LAST) || /* Braille */ (base >= GLYPH_BRAILLE_FIRST && base <= GLYPH_BRAILLE_LAST) || /* * Unicode 13 "Symbols for Legacy Computing" * sub-ranges below. * * Note, the full range is U+1FB00 - U+1FBF9 */ (base >= GLYPH_LEGACY_FIRST && base <= GLYPH_LEGACY_LAST) || /* * Unicode 16 "Symbols for Legacy Computing Supplement" * * Note, the full range is U+1CC00 - U+1CEAF */ (base >= GLYPH_OCTANTS_FIRST && base <= GLYPH_OCTANTS_LAST)) && likely(!term->conf->box_drawings_uses_font_glyphs)) { struct fcft_glyph ***arr; size_t count; size_t idx; if (base >= GLYPH_LEGACY_FIRST) { arr = &term->custom_glyphs.legacy; count = GLYPH_LEGACY_COUNT; idx = base - GLYPH_LEGACY_FIRST; } else if (base >= GLYPH_OCTANTS_FIRST) { arr = &term->custom_glyphs.octants; count = GLYPH_OCTANTS_COUNT; idx = base - GLYPH_OCTANTS_FIRST; } else if (base >= GLYPH_BRAILLE_FIRST) { arr = &term->custom_glyphs.braille; count = GLYPH_BRAILLE_COUNT; idx = base - GLYPH_BRAILLE_FIRST; } else { arr = &term->custom_glyphs.box_drawing; count = GLYPH_BOX_DRAWING_COUNT; idx = base - GLYPH_BOX_DRAWING_FIRST; } if (unlikely(*arr == NULL)) *arr = xcalloc(count, sizeof((*arr)[0])); if (likely((*arr)[idx] != NULL)) single = (*arr)[idx]; else { mtx_lock(&term->render.workers.lock); /* Other thread may have instantiated it while we * acquired the lock */ single = (*arr)[idx]; if (likely(single == NULL)) single = (*arr)[idx] = box_drawing(term, base); mtx_unlock(&term->render.workers.lock); } if (single != NULL) { glyph_count = 1; glyphs = &single; cell_cols = single->cols; } } else if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) { composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); base = composed->chars[0]; if (term->conf->can_shape_grapheme && term->conf->tweak.grapheme_shaping) { grapheme = fcft_rasterize_grapheme_utf32( font, composed->count, composed->chars, term->font_subpixel); } if (grapheme != NULL) { const int forced_width = composed->forced_width; cell_cols = forced_width > 0 ? forced_width : composed->width; composed = NULL; glyphs = grapheme->glyphs; glyph_count = grapheme->count; if (forced_width > 0) glyph_count = min(glyph_count, forced_width); } } if (single == NULL && grapheme == NULL) { if (unlikely(base >= CELL_SPACER)) { glyph_count = 0; cell_cols = 1; } else { xassert(base != 0); single = fcft_rasterize_char_utf32(font, base, term->font_subpixel); if (single == NULL) { glyph_count = 0; cell_cols = 1; } else { glyph_count = 1; glyphs = &single; const size_t forced_width = composed != NULL ? composed->forced_width : 0; cell_cols = forced_width > 0 ? forced_width : single->cols; } } } } assert(glyph_count == 0 || glyphs != NULL); const int cols_left = term->cols - col; cell_cols = max(1, min(cell_cols, cols_left)); /* * Determine cells that will bleed into their right neighbor and remember * them for cleanup in the next frame. */ int render_width = cell_cols * width; if (term->conf->tweak.overflowing_glyphs && glyph_count > 0 && cols_left > cell_cols) { int glyph_width = 0, advance = 0; for (size_t i = 0; i < glyph_count; i++) { glyph_width = max(glyph_width, advance + glyphs[i]->x + glyphs[i]->width); advance += glyphs[i]->advance.x; } if (glyph_width > render_width) { render_width = min(glyph_width, render_width + width); for (int i = 0; i < cell_cols; i++) row->cells[col + i].attrs.confined = false; } } pixman_region32_t clip; pixman_region32_init_rect( &clip, x, y, render_width, term->cell_height); pixman_image_set_clip_region32(pix, &clip); if (damage != NULL) { pixman_region32_union_rect( damage, damage, x, y, render_width, term->cell_height); } pixman_region32_fini(&clip); /* Background */ pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, &bg, 1, &(pixman_rectangle16_t){x, y, cell_cols * width, height}); if (cell->attrs.blink && term->blink.fd < 0) { /* TODO: use a custom lock for this? */ mtx_lock(&term->render.workers.lock); term_arm_blink_timer(term); mtx_unlock(&term->render.workers.lock); } if (unlikely(has_cursor && term->cursor_style == CURSOR_BLOCK && term->kbd_focus)) draw_cursor(term, cell, font, pix, &fg, &bg, x, y, cell_cols); if (cell->wc == 0 || cell->wc >= CELL_SPACER || cell->wc == U'\t' || (unlikely(cell->attrs.conceal) && !is_selected)) { goto draw_cursor; } pixman_image_t *clr_pix = pixman_image_create_solid_fill(&fg); int pen_x = x; for (unsigned i = 0; i < glyph_count; i++) { const int letter_x_ofs = i == 0 ? term->font_x_ofs : 0; const struct fcft_glyph *glyph = glyphs[i]; if (glyph == NULL) continue; int g_x = glyph->x; int g_y = glyph->y; if (i > 0 && glyph->x >= 0 && cell_cols == 1) g_x -= term->cell_width; if (unlikely(glyph->is_color_glyph)) { /* Glyph surface is a pre-rendered image (typically a color emoji...) */ if (!(cell->attrs.blink && term->blink.state == BLINK_OFF)) { pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, pix, 0, 0, 0, 0, pen_x + letter_x_ofs + g_x, y + term->font_baseline - g_y, glyph->width, glyph->height); } } else { pixman_image_composite32( PIXMAN_OP_OVER, clr_pix, glyph->pix, pix, 0, 0, 0, 0, pen_x + letter_x_ofs + g_x, y + term->font_baseline - g_y, glyph->width, glyph->height); /* Combining characters */ if (composed != NULL) { assert(glyph_count == 1); for (size_t j = 1; j < composed->count; j++) { const struct fcft_glyph *g = fcft_rasterize_char_utf32( font, composed->chars[j], term->font_subpixel); if (g == NULL) continue; /* * Fonts _should_ assume the pen position is now * *after* the base glyph, and thus use negative * offsets for combining glyphs. * * Not all fonts behave like this however, and we * try to accommodate both variants. * * Since we haven't moved our pen position yet, we * add a full cell width to the offset (or two, in * case of double-width characters). * * If the font does *not* use negative offsets, * we'd normally use an offset of 0. However, to * somewhat deal with double-width glyphs we use * an offset of *one* cell. */ int x_ofs = cell_cols == 1 ? g->x < 0 ? cell_cols * term->cell_width : (cell_cols - 1) * term->cell_width : 0; if (cell_cols > 1) pen_x += term->cell_width; pixman_image_composite32( PIXMAN_OP_OVER, clr_pix, g->pix, pix, 0, 0, 0, 0, /* Some fonts use a negative offset, while others use a * "normal" offset */ pen_x + letter_x_ofs + x_ofs + g->x, y + term->font_baseline - g->y, g->width, g->height); } } } pen_x += cell_cols > 1 ? term->cell_width : glyph->advance.x; } pixman_image_unref(clr_pix); /* Underline */ if (cell->attrs.underline) { pixman_color_t underline_color = fg; enum underline_style underline_style = UNDERLINE_SINGLE; /* Check if cell has a styled underline. This lookup is fairly expensive... */ if (row->extra != NULL) { for (int i = 0; i < row->extra->underline_ranges.count; i++) { const struct row_range *range = &row->extra->underline_ranges.v[i]; if (range->start > col) break; if (range->start <= col && col <= range->end) { switch (range->underline.color_src) { case COLOR_BASE256: underline_color = color_hex_to_pixman( term->colors.table[range->underline.color], gamma_correct); break; case COLOR_RGB: underline_color = color_hex_to_pixman(range->underline.color, gamma_correct); break; case COLOR_DEFAULT: break; case COLOR_BASE16: BUG("underline color can't be base-16"); break; } underline_style = range->underline.style; break; } } } draw_styled_underline( term, pix, font, &underline_color, underline_style, x, y, cell_cols); } if (cell->attrs.strikethrough) draw_strikeout(term, pix, font, &fg, x, y, cell_cols); if (unlikely(cell->attrs.url)) { pixman_color_t url_color = color_hex_to_pixman( term->conf->colors.use_custom.url ? term->conf->colors.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); pixman_image_set_clip_region32(pix, NULL); return cell_cols; } static void render_row(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damage, struct row *row, int row_no, int cursor_col) { for (int col = term->cols - 1; col >= 0; col--) render_cell(term, pix, damage, row, row_no, col, cursor_col == col); } static void render_urgency(struct terminal *term, struct buffer *buf) { uint32_t red = term->colors.table[1]; pixman_color_t bg = color_hex_to_pixman(red, render_do_linear_blending(term)); int width = min(min(term->margins.left, term->margins.right), min(term->margins.top, term->margins.bottom)); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &bg, 4, (pixman_rectangle16_t[]){ /* Top */ {0, 0, term->width, width}, /* Bottom */ {0, term->height - width, term->width, width}, /* Left */ {0, width, width, term->height - 2 * width}, /* Right */ {term->width - width, width, width, term->height - 2 * width}, }); } static void render_margin(struct terminal *term, struct buffer *buf, int start_line, int end_line, bool apply_damage) { /* Fill area outside the cell grid with the default background color */ const int rmargin = term->width - term->margins.right; const int bmargin = term->height - term->margins.bottom; const int line_count = end_line - start_line; const bool gamma_correct = render_do_linear_blending(term); const uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; uint16_t alpha = term->colors.alpha; if (term->window->is_fullscreen) { /* Disable alpha in fullscreen - see render_cell() for details */ alpha = 0xffff; } pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &bg, 4, (pixman_rectangle16_t[]){ /* Top */ {0, 0, term->width, term->margins.top}, /* Bottom */ {0, bmargin, term->width, term->margins.bottom}, /* Left */ {0, term->margins.top + start_line * term->cell_height, term->margins.left, line_count * term->cell_height}, /* Right */ {rmargin, term->margins.top + start_line * term->cell_height, term->margins.right, line_count * term->cell_height}, }); if (term->render.urgency) render_urgency(term, buf); /* Ensure the updated regions are copied to the next frame's * buffer when we're double buffering */ pixman_region32_union_rect( &buf->dirty[0], &buf->dirty[0], 0, 0, term->width, term->margins.top); pixman_region32_union_rect( &buf->dirty[0], &buf->dirty[0], 0, bmargin, term->width, term->margins.bottom); pixman_region32_union_rect( &buf->dirty[0], &buf->dirty[0], 0, 0, term->margins.left, term->height); pixman_region32_union_rect( &buf->dirty[0], &buf->dirty[0], rmargin, 0, term->margins.right, term->height); if (apply_damage) { /* Top */ wl_surface_damage_buffer( term->window->surface.surf, 0, 0, term->width, term->margins.top); /* Bottom */ wl_surface_damage_buffer( term->window->surface.surf, 0, bmargin, term->width, term->margins.bottom); /* Left */ wl_surface_damage_buffer( term->window->surface.surf, 0, term->margins.top + start_line * term->cell_height, term->margins.left, line_count * term->cell_height); /* Right */ wl_surface_damage_buffer( term->window->surface.surf, rmargin, term->margins.top + start_line * term->cell_height, term->margins.right, line_count * term->cell_height); } } static void grid_render_scroll(struct terminal *term, struct buffer *buf, const struct damage *dmg) { LOG_DBG( "damage: SCROLL: %d-%d by %d lines", dmg->region.start, dmg->region.end, dmg->lines); const int region_size = dmg->region.end - dmg->region.start; if (dmg->lines >= region_size) { /* The entire scroll region will be scrolled out (i.e. replaced) */ return; } const int height = (region_size - dmg->lines) * term->cell_height; xassert(height > 0); #if TIME_SCROLL_DAMAGE struct timespec start_time; clock_gettime(CLOCK_MONOTONIC, &start_time); #endif int dst_y = term->margins.top + (dmg->region.start + 0) * term->cell_height; int src_y = term->margins.top + (dmg->region.start + dmg->lines) * term->cell_height; /* * SHM scrolling can be *much* faster, but it depends on how many * lines we're scrolling, and how much repairing we need to do. * * In short, scrolling a *large* number of rows is faster with a * memmove, while scrolling a *small* number of lines is faster * with SHM scrolling. * * However, since we need to restore the scrolling regions when * SHM scrolling, we also need to take this into account. * * Finally, we also have to restore the window margins, and this * is a *huge* performance hit when scrolling a large number of * lines (in addition to the sloweness of SHM scrolling as * method). * * So, we need to figure out when to SHM scroll, and when to * memmove. * * For now, assume that the both methods perform roughly the same, * given an equal number of bytes to move/allocate, and use the * method that results in the least amount of bytes to touch. * * Since number of lines directly translates to bytes, we can * simply count lines. * * SHM scrolling needs to first "move" (punch hole + allocate) * dmg->lines number of lines, and then we need to restore * the bottom scroll region. * * If the total number of lines is less than half the screen - use * SHM. Otherwise use memmove. */ bool try_shm_scroll = shm_can_scroll(buf) && ( dmg->lines + dmg->region.start + (term->rows - dmg->region.end)) < term->rows / 2; bool did_shm_scroll = false; //try_shm_scroll = false; //try_shm_scroll = true; if (try_shm_scroll) { did_shm_scroll = shm_scroll( buf, dmg->lines * term->cell_height, term->margins.top, dmg->region.start * term->cell_height, term->margins.bottom, (term->rows - dmg->region.end) * term->cell_height); } if (did_shm_scroll) { /* Restore margins */ render_margin( term, buf, dmg->region.end - dmg->lines, term->rows, false); } else { /* Fallback for when we either cannot do SHM scrolling, or it failed */ uint8_t *raw = buf->data; memmove(raw + dst_y * buf->stride, raw + src_y * buf->stride, height * buf->stride); } #if TIME_SCROLL_DAMAGE struct timespec end_time; clock_gettime(CLOCK_MONOTONIC, &end_time); struct timespec memmove_time; timespec_sub(&end_time, &start_time, &memmove_time); LOG_INFO("scrolled %dKB (%d lines) using %s in %lds %ldns", height * buf->stride / 1024, dmg->lines, did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove", (long)memmove_time.tv_sec, memmove_time.tv_nsec); #endif wl_surface_damage_buffer( term->window->surface.surf, term->margins.left, dst_y, term->width - term->margins.left - term->margins.right, height); /* * TODO: remove this if re-enabling scroll damage when re-applying * last frame's damage (see reapply_old_damage() */ pixman_region32_union_rect( &buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); } static void grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, const struct damage *dmg) { LOG_DBG( "damage: SCROLL REVERSE: %d-%d by %d lines", dmg->region.start, dmg->region.end, dmg->lines); const int region_size = dmg->region.end - dmg->region.start; if (dmg->lines >= region_size) { /* The entire scroll region will be scrolled out (i.e. replaced) */ return; } const int height = (region_size - dmg->lines) * term->cell_height; xassert(height > 0); #if TIME_SCROLL_DAMAGE struct timespec start_time; clock_gettime(CLOCK_MONOTONIC, &start_time); #endif int src_y = term->margins.top + (dmg->region.start + 0) * term->cell_height; int dst_y = term->margins.top + (dmg->region.start + dmg->lines) * term->cell_height; bool try_shm_scroll = shm_can_scroll(buf) && ( dmg->lines + dmg->region.start + (term->rows - dmg->region.end)) < term->rows / 2; bool did_shm_scroll = false; if (try_shm_scroll) { did_shm_scroll = shm_scroll( buf, -dmg->lines * term->cell_height, term->margins.top, dmg->region.start * term->cell_height, term->margins.bottom, (term->rows - dmg->region.end) * term->cell_height); } if (did_shm_scroll) { /* Restore margins */ render_margin( term, buf, dmg->region.start, dmg->region.start + dmg->lines, false); } else { /* Fallback for when we either cannot do SHM scrolling, or it failed */ uint8_t *raw = buf->data; memmove(raw + dst_y * buf->stride, raw + src_y * buf->stride, height * buf->stride); } #if TIME_SCROLL_DAMAGE struct timespec end_time; clock_gettime(CLOCK_MONOTONIC, &end_time); struct timespec memmove_time; timespec_sub(&end_time, &start_time, &memmove_time); LOG_INFO("scrolled REVERSE %dKB (%d lines) using %s in %lds %ldns", height * buf->stride / 1024, dmg->lines, did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove", (long)memmove_time.tv_sec, memmove_time.tv_nsec); #endif wl_surface_damage_buffer( term->window->surface.surf, term->margins.left, dst_y, term->width - term->margins.left - term->margins.right, height); /* * TODO: remove this if re-enabling scroll damage when re-applying * last frame's damage (see reapply_old_damage() */ pixman_region32_union_rect( &buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); } static void render_sixel_chunk(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damage, const struct sixel *sixel, int term_start_row, int img_start_row, int count) { /* Translate row/column to x/y pixel values */ const int x = term->margins.left + sixel->pos.col * term->cell_width; const int y = term->margins.top + term_start_row * term->cell_height; /* Width/height, in pixels - and don't touch the window margins */ const int width = max( 0, min(sixel->width, term->width - x - term->margins.right)); const int height = max( 0, min( min(count * term->cell_height, /* 'count' number of rows */ sixel->height - img_start_row * term->cell_height), /* What remains of the sixel */ term->height - y - term->margins.bottom)); /* Verify we're not stepping outside the grid */ xassert(x >= term->margins.left); xassert(y >= term->margins.top); xassert(width == 0 || x + width <= term->width - term->margins.right); xassert(height == 0 || y + height <= term->height - term->margins.bottom); //LOG_DBG("sixel chunk: %dx%d %dx%d", x, y, width, height); pixman_image_composite32( sixel->opaque ? PIXMAN_OP_SRC : PIXMAN_OP_OVER, sixel->pix, NULL, pix, 0, img_start_row * term->cell_height, 0, 0, x, y, width, height); if (damage != NULL) pixman_region32_union_rect(damage, damage, x, y, width, height); } static void render_sixel(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damage, const struct coord *cursor, const struct sixel *sixel) { xassert(sixel->pix != NULL); xassert(sixel->width >= 0); xassert(sixel->height >= 0); const int view_end = (term->grid->view + term->rows - 1) & (term->grid->num_rows - 1); const bool last_row_needs_erase = sixel->height % term->cell_height != 0; const bool last_col_needs_erase = sixel->width % term->cell_width != 0; int chunk_img_start = -1; /* Image-relative start row of chunk */ int chunk_term_start = -1; /* Viewport relative start row of chunk */ int chunk_row_count = 0; /* Number of rows to emit */ #define maybe_emit_sixel_chunk_then_reset() \ if (chunk_row_count != 0) { \ render_sixel_chunk( \ term, pix, damage, sixel, \ chunk_term_start, chunk_img_start, chunk_row_count); \ chunk_term_start = chunk_img_start = -1; \ chunk_row_count = 0; \ } /* * Iterate all sixel rows: * * - ignore rows that aren't visible on-screen * - ignore rows that aren't dirty (they have already been rendered) * - chunk consecutive dirty rows into a 'chunk' * - emit (render) chunk as soon as a row isn't visible, or is clean * - emit final chunk after we've iterated all rows * * The purpose of this is to reduce the amount of pixels that * needs to be composited and marked as damaged for the * compositor. * * Since we do CPU based composition, rendering is a slow and * heavy task for foot, and thus it is important to not re-render * things unnecessarily. */ for (int _abs_row_no = sixel->pos.row; _abs_row_no < sixel->pos.row + sixel->rows; _abs_row_no++) { const int abs_row_no = _abs_row_no & (term->grid->num_rows - 1); const int term_row_no = (abs_row_no - term->grid->view + term->grid->num_rows) & (term->grid->num_rows - 1); /* Check if row is in the visible viewport */ if (view_end >= term->grid->view) { /* Not wrapped */ if (!(abs_row_no >= term->grid->view && abs_row_no <= view_end)) { /* Not visible */ maybe_emit_sixel_chunk_then_reset(); continue; } } else { /* Wrapped */ if (!(abs_row_no >= term->grid->view || abs_row_no <= view_end)) { /* Not visible */ maybe_emit_sixel_chunk_then_reset(); continue; } } /* Is the row dirty? */ struct row *row = term->grid->rows[abs_row_no]; xassert(row != NULL); /* Should be visible */ if (!row->dirty) { maybe_emit_sixel_chunk_then_reset(); continue; } int cursor_col = cursor->row == term_row_no ? cursor->col : -1; /* * If image contains transparent parts, render all (dirty) * cells beneath it. * * If image is opaque, loop cells and set their 'clean' bit, * to prevent the grid rendered from overwriting the sixel * * If the last sixel row only partially covers the cell row, * 'erase' the cell by rendering them. * * In all cases, do *not* clear the 'dirty' bit on the row, to * ensure the regular renderer includes them in the damage * rect. */ if (!sixel->opaque) { /* TODO: multithreading */ render_row(term, pix, damage, row, term_row_no, cursor_col); } else { for (int col = sixel->pos.col; col < min(sixel->pos.col + sixel->cols, term->cols); col++) { struct cell *cell = &row->cells[col]; if (!cell->attrs.clean) { bool last_row = abs_row_no == sixel->pos.row + sixel->rows - 1; bool last_col = col == sixel->pos.col + sixel->cols - 1; if ((last_row_needs_erase && last_row) || (last_col_needs_erase && last_col)) { render_cell(term, pix, damage, row, term_row_no, col, cursor_col == col); } else { cell->attrs.clean = 1; cell->attrs.confined = 1; } } } } if (chunk_term_start == -1) { xassert(chunk_img_start == -1); chunk_term_start = term_row_no; chunk_img_start = _abs_row_no - sixel->pos.row; chunk_row_count = 1; } else chunk_row_count++; } maybe_emit_sixel_chunk_then_reset(); #undef maybe_emit_sixel_chunk_then_reset } static void render_sixel_images(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damage, const struct coord *cursor) { if (likely(tll_length(term->grid->sixel_images)) == 0) return; const int scrollback_end = (term->grid->offset + term->rows) & (term->grid->num_rows - 1); const int view_start = (term->grid->view - scrollback_end + term->grid->num_rows) & (term->grid->num_rows - 1); const int view_end = view_start + term->rows - 1; //LOG_DBG("SIXELS: %zu images, view=%d-%d", // tll_length(term->grid->sixel_images), view_start, view_end); tll_foreach(term->grid->sixel_images, it) { const struct sixel *six = &it->item; const int start = (six->pos.row - scrollback_end + term->grid->num_rows) & (term->grid->num_rows - 1); const int end = start + six->rows - 1; //LOG_DBG(" sixel: %d-%d", start, end); if (start > view_end) { /* Sixel starts after view ends, no need to try to render it */ continue; } else if (end < view_start) { /* Image ends before view starts. Since the image list is * sorted, we can safely stop here */ break; } sixel_sync_cache(term, &it->item); render_sixel(term, pix, damage, cursor, &it->item); } } #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED static void render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, struct buffer *buf) { if (likely(seat->ime.preedit.cells == NULL)) return; if (unlikely(term->is_searching)) return; const bool gamma_correct = render_do_linear_blending(term); /* Adjust cursor position to viewport */ struct coord cursor; cursor = term->grid->cursor.point; cursor.row += term->grid->offset; cursor.row -= term->grid->view; cursor.row &= term->grid->num_rows - 1; if (cursor.row < 0 || cursor.row >= term->rows) return; int cells_needed = seat->ime.preedit.count; if (seat->ime.preedit.cursor.start == cells_needed && seat->ime.preedit.cursor.end == cells_needed) { /* Cursor will be drawn *after* the pre-edit string, i.e. in * the cell *after*. This means we need to copy, and dirty, * one extra cell from the original grid, or we'll leave * trailing "cursors" after us if the user deletes text while * pre-editing */ cells_needed++; } int row_idx = cursor.row; int col_idx = cursor.col; int ime_ofs = 0; /* Offset into pre-edit string to start rendering at */ int cells_left = term->cols - cursor.col; int cells_used = min(cells_needed, term->cols); /* Adjust start of pre-edit text to the left if string doesn't fit on row */ if (cells_left < cells_used) col_idx -= cells_used - cells_left; if (cells_needed > cells_used) { int start = seat->ime.preedit.cursor.start; int end = seat->ime.preedit.cursor.end; if (start == end) { /* Ensure *end* of pre-edit string is visible */ ime_ofs = cells_needed - cells_used; } else { /* Ensure the *beginning* of the cursor-area is visible */ ime_ofs = start; /* Display as much as possible of the pre-edit string */ if (cells_needed - ime_ofs < cells_used) ime_ofs = cells_needed - cells_used; } /* Make sure we don't start in the middle of a character */ while (ime_ofs < cells_needed && seat->ime.preedit.cells[ime_ofs].wc >= CELL_SPACER) { ime_ofs++; } } xassert(col_idx >= 0); xassert(col_idx < term->cols); struct row *row = grid_row_in_view(term->grid, row_idx); /* Don't start pre-edit text in the middle of a double-width character */ while (col_idx > 0 && row->cells[col_idx].wc >= CELL_SPACER) { cells_used++; col_idx--; } /* * Copy original content (render_cell() reads cell data directly * from grid), and mark all cells as dirty. This ensures they are * re-rendered when the pre-edit text is modified or removed. */ struct cell *real_cells = xmalloc(cells_used * sizeof(real_cells[0])); for (int i = 0; i < cells_used; i++) { xassert(col_idx + i < term->cols); real_cells[i] = row->cells[col_idx + i]; real_cells[i].attrs.clean = 0; } row->dirty = true; /* Render pre-edit text */ xassert(seat->ime.preedit.cells[ime_ofs].wc < CELL_SPACER); for (int i = 0, idx = ime_ofs; idx < seat->ime.preedit.count; i++, idx++) { const struct cell *cell = &seat->ime.preedit.cells[idx]; if (cell->wc >= CELL_SPACER) continue; int width = max(1, c32width(cell->wc)); if (col_idx + i + width > term->cols) break; row->cells[col_idx + i] = *cell; render_cell(term, buf->pix[0], NULL, row, row_idx, col_idx + i, false); } int start = seat->ime.preedit.cursor.start - ime_ofs; int end = seat->ime.preedit.cursor.end - ime_ofs; if (!seat->ime.preedit.cursor.hidden) { const struct cell *start_cell = &seat->ime.preedit.cells[0]; pixman_color_t fg = color_hex_to_pixman(term->colors.fg, gamma_correct); pixman_color_t bg = color_hex_to_pixman(term->colors.bg, gamma_correct); pixman_color_t cursor_color, text_color; cursor_colors_for_cell( term, start_cell, &fg, &bg, &cursor_color, &text_color, gamma_correct); int x = term->margins.left + (col_idx + start) * term->cell_width; int y = term->margins.top + row_idx * term->cell_height; if (end == start) { /* Bar */ if (start >= 0) { struct fcft_font *font = attrs_to_font(term, &start_cell->attrs); draw_beam_cursor(term, buf->pix[0], font, &cursor_color, x, y); } term_ime_set_cursor_rect(term, x, y, 1, term->cell_height); } else if (end > start) { /* Hollow cursor */ if (start >= 0 && end <= term->cols) { int cols = end - start; draw_hollow_block(term, buf->pix[0], &cursor_color, x, y, cols); } term_ime_set_cursor_rect( term, x, y, (end - start) * term->cell_width, term->cell_height); } } /* Restore original content (but do not render) */ for (int i = 0; i < cells_used; i++) row->cells[col_idx + i] = real_cells[i]; free(real_cells); const int damage_x = term->margins.left + col_idx * term->cell_width; const int damage_y = term->margins.top + row_idx * term->cell_height; const int damage_w = cells_used * term->cell_width; const int damage_h = term->cell_height; wl_surface_damage_buffer( term->window->surface.surf, damage_x, damage_y, damage_w, damage_h); } #endif static void render_ime_preedit(struct terminal *term, struct buffer *buf) { #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) render_ime_preedit_for_seat(term, &it->item, buf); } #endif } static void render_overlay_single_pixel(struct terminal *term, enum overlay_style style, pixman_color_t color) { struct wayland *wayl = term->wl; struct wayl_sub_surface *overlay = &term->window->overlay; struct wl_buffer *buf = NULL; /* * In an ideal world, we'd only update the surface (i.e. commit * any changes) if anything has actually changed. * * For technical reasons, we can't do that, since we can't * determine whether the last committed buffer is still valid * (i.e. does it correspond to the current overlay style, *and* * does last frame's size match the current size?) * * What we _can_ do is use the fact that single-pixel buffers * don't have a size; you have to use a viewport to "size" them. * * This means we can check if the last frame's overlay style is * the same as the current size. If so, then we *know* that the * currently attached buffer is valid, and we *don't* have to * create a new single-pixel buffer. * * What we do *not* know if the *size* is still valid. This means * we do have to do the viewport calls, and a surface commit. * * This is still better than *always* creating a new buffer. */ assert(style == OVERLAY_UNICODE_MODE || style == OVERLAY_FLASH); assert(wayl->single_pixel_manager != NULL); assert(overlay->surface.viewport != NULL); quirk_weston_subsurface_desync_on(overlay->sub); if (style != term->render.last_overlay_style) { buf = wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer( wayl->single_pixel_manager, (double)color.red / 0xffff * 0xffffffff, (double)color.green / 0xffff * 0xffffffff, (double)color.blue / 0xffff * 0xffffffff, (double)color.alpha / 0xffff * 0xffffffff); wl_surface_set_buffer_scale(overlay->surface.surf, 1); wl_surface_attach(overlay->surface.surf, buf, 0, 0); } wp_viewport_set_destination( overlay->surface.viewport, roundf(term->width / term->scale), roundf(term->height / term->scale)); wl_subsurface_set_position(overlay->sub, 0, 0); wl_surface_damage_buffer( overlay->surface.surf, 0, 0, term->width, term->height); wl_surface_commit(overlay->surface.surf); quirk_weston_subsurface_desync_off(overlay->sub); term->render.last_overlay_style = style; if (buf != NULL) { wl_buffer_destroy(buf); } } void render_overlay(struct terminal *term) { struct wayl_sub_surface *overlay = &term->window->overlay; const bool unicode_mode_active = term->unicode_mode.active; const enum overlay_style style = term->is_searching ? OVERLAY_SEARCH : term->flash.active ? OVERLAY_FLASH : unicode_mode_active ? OVERLAY_UNICODE_MODE : OVERLAY_NONE; if (likely(style == OVERLAY_NONE)) { if (term->render.last_overlay_style != OVERLAY_NONE) { /* Unmap overlay sub-surface */ wl_surface_attach(overlay->surface.surf, NULL, 0, 0); wl_surface_commit(overlay->surface.surf); term->render.last_overlay_style = OVERLAY_NONE; term->render.last_overlay_buf = NULL; /* Work around Sway bug - unmapping a sub-surface does not * damage the underlying surface */ quirk_sway_subsurface_unmap(term); } return; } pixman_color_t color; switch (style) { case OVERLAY_SEARCH: case OVERLAY_UNICODE_MODE: color = (pixman_color_t){0, 0, 0, 0x7fff}; break; case OVERLAY_FLASH: color = color_hex_to_pixman_with_alpha( term->conf->colors.flash, term->conf->colors.flash_alpha, render_do_linear_blending(term)); 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, true); pixman_image_set_clip_region32(buf->pix[0], NULL); /* Bounding rectangle of damaged areas - for wl_surface_damage_buffer() */ pixman_box32_t damage_bounds; if (style == OVERLAY_SEARCH) { /* * When possible, we only update the areas that have *changed* * since the last frame. That means: * * - clearing/erasing cells that are now selected, but weren't * in the last frame * - dimming cells that were selected, but aren't anymore * * To do this, we save the last frame's selected cells as a * pixman region. * * Then, we calculate the corresponding region for this * frame's selected cells. * * Last frame's region minus this frame's region gives us the * region that needs to be *dimmed* in this frame * * This frame's region minus last frame's region gives us the * region that needs to be *cleared* in this frame. * * Finally, the union of the two "diff" regions above, gives * us the total region affected by a change, in either way. We * use this as the bounding box for the * wl_surface_damage_buffer() call. */ pixman_region32_t *see_through = &term->render.last_overlay_clip; pixman_region32_t old_see_through; const bool buffer_reuse = buf == term->render.last_overlay_buf && style == term->render.last_overlay_style && buf->age == 0; if (!buffer_reuse) { /* Can't reuse last frame's damage - set to full window, * to ensure *everything* is updated */ pixman_region32_init_rect( &old_see_through, 0, 0, buf->width, buf->height); } else { /* Use last frame's saved region */ pixman_region32_init(&old_see_through); pixman_region32_copy(&old_see_through, see_through); } pixman_region32_clear(see_through); /* Build region consisting of all current search matches */ struct search_match_iterator iter = search_matches_new_iter(term); for (struct range match = search_matches_next(&iter); match.start.row >= 0; match = search_matches_next(&iter)) { int r = match.start.row; int start_col = match.start.col; const int end_row = match.end.row; while (true) { const int end_col = r == end_row ? match.end.col : term->cols - 1; int x = term->margins.left + start_col * term->cell_width; int y = term->margins.top + r * term->cell_height; int width = (end_col + 1 - start_col) * term->cell_width; int height = 1 * term->cell_height; pixman_region32_union_rect( see_through, see_through, x, y, width, height); if (++r > end_row) break; start_col = 0; } } /* Areas that need to be cleared: cells that were dimmed in * the last frame but is now see-through */ pixman_region32_t new_see_through; pixman_region32_init(&new_see_through); if (buffer_reuse) pixman_region32_subtract(&new_see_through, see_through, &old_see_through); else { /* Buffer content is unknown - explicitly clear *all* * current see-through areas */ pixman_region32_copy(&new_see_through, see_through); } pixman_image_set_clip_region32(buf->pix[0], &new_see_through); /* Areas that need to be dimmed: cells that were cleared in * the last frame but is not anymore */ pixman_region32_t new_dimmed; pixman_region32_init(&new_dimmed); pixman_region32_subtract(&new_dimmed, &old_see_through, see_through); pixman_region32_fini(&old_see_through); /* Total affected area */ pixman_region32_t damage; pixman_region32_init(&damage); pixman_region32_union(&damage, &new_see_through, &new_dimmed); damage_bounds = damage.extents; /* Clear cells that became selected in this frame. */ pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &(pixman_color_t){0}, 1, &(pixman_rectangle16_t){0, 0, term->width, term->height}); /* Set clip region for the newly dimmed cells. The actual * paint call is done below */ pixman_image_set_clip_region32(buf->pix[0], &new_dimmed); pixman_region32_fini(&new_see_through); pixman_region32_fini(&new_dimmed); pixman_region32_fini(&damage); } else if (buf == term->render.last_overlay_buf && style == term->render.last_overlay_style) { xassert(style == OVERLAY_FLASH || style == OVERLAY_UNICODE_MODE); shm_did_not_use_buf(buf); return; } else { pixman_image_set_clip_region32(buf->pix[0], NULL); damage_bounds = (pixman_box32_t){0, 0, buf->width, buf->height}; } pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 1, &(pixman_rectangle16_t){0, 0, term->width, term->height}); quirk_weston_subsurface_desync_on(overlay->sub); wayl_surface_scale( term->window, &overlay->surface, buf, term->scale); wl_subsurface_set_position(overlay->sub, 0, 0); wl_surface_attach(overlay->surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer( overlay->surface.surf, damage_bounds.x1, damage_bounds.y1, damage_bounds.x2 - damage_bounds.x1, damage_bounds.y2 - damage_bounds.y1); wl_surface_commit(overlay->surface.surf); quirk_weston_subsurface_desync_off(overlay->sub); buf->age = 0; term->render.last_overlay_buf = buf; term->render.last_overlay_style = style; } int render_worker_thread(void *_ctx) { struct render_worker_context *ctx = _ctx; struct terminal *term = ctx->term; const int my_id = ctx->my_id; free(ctx); sigset_t mask; sigfillset(&mask); pthread_sigmask(SIG_SETMASK, &mask, NULL); char proc_title[16]; snprintf(proc_title, sizeof(proc_title), "foot:render:%d", my_id); if (pthread_setname_np(pthread_self(), proc_title) < 0) LOG_ERRNO("render worker %d: failed to set process title", my_id); sem_t *start = &term->render.workers.start; sem_t *done = &term->render.workers.done; mtx_t *lock = &term->render.workers.lock; while (true) { sem_wait(start); struct buffer *buf = term->render.workers.buf; bool frame_done = false; /* Translate offset-relative cursor row to view-relative */ struct coord cursor = {-1, -1}; if (!term->hide_cursor) { cursor = term->grid->cursor.point; cursor.row += term->grid->offset; cursor.row -= term->grid->view; cursor.row &= term->grid->num_rows - 1; } while (!frame_done) { mtx_lock(lock); xassert(tll_length(term->render.workers.queue) > 0); int row_no = tll_pop_front(term->render.workers.queue); mtx_unlock(lock); switch (row_no) { default: { struct row *row = grid_row_in_view(term->grid, row_no); int cursor_col = cursor.row == row_no ? cursor.col : -1; render_row(term, buf->pix[my_id], &buf->dirty[my_id], row, row_no, cursor_col); break; } case -1: frame_done = true; sem_post(done); break; case -2: return 0; } } }; return -1; } struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx) { xassert(term->window->csd_mode == CSD_YES); const bool borders_visible = wayl_win_csd_borders_visible(term->window); const bool title_visible = wayl_win_csd_titlebar_visible(term->window); const float scale = term->scale; const int border_width = borders_visible ? roundf(term->conf->csd.border_width * scale) : 0; const int title_height = title_visible ? roundf(term->conf->csd.title_height * scale) : 0; const int button_width = title_visible ? roundf(term->conf->csd.button_width * scale) : 0; const int button_close_width = term->width >= 1 * button_width ? button_width : 0; const int button_maximize_width = term->width >= 2 * button_width && term->window->wm_capabilities.maximize ? button_width : 0; const int button_minimize_width = term->width >= 3 * button_width && term->window->wm_capabilities.minimize ? button_width : 0; /* * With fractional scaling, we must ensure the offset, when * divided by the scale (in set_position()), and the scaled back * (by the compositor), matches the actual pixel count made up by * the titlebar and the border. */ const int top_offset = roundf( scale * (roundf(-title_height / scale) - roundf(border_width / scale))); const int top_bottom_width = roundf( scale * (roundf(term->width / scale) + 2 * roundf(border_width / scale))); const int left_right_height = roundf( scale * (roundf(title_height / scale) + roundf(term->height / scale))); switch (surf_idx) { case CSD_SURF_TITLE: return (struct csd_data){ 0, -title_height, term->width, title_height}; case CSD_SURF_LEFT: return (struct csd_data){-border_width, -title_height, border_width, left_right_height}; case CSD_SURF_RIGHT: return (struct csd_data){ term->width, -title_height, border_width, left_right_height}; case CSD_SURF_TOP: return (struct csd_data){-border_width, top_offset, top_bottom_width, border_width}; case CSD_SURF_BOTTOM: return (struct csd_data){-border_width, term->height, top_bottom_width, border_width}; /* Positioned relative to CSD_SURF_TITLE */ case CSD_SURF_MINIMIZE: return (struct csd_data){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_COUNT: break; } BUG("Invalid csd_surface type"); return (struct csd_data){0}; } static void csd_commit(struct terminal *term, struct wayl_surface *surf, struct buffer *buf) { wayl_surface_scale(term->window, surf, buf, term->scale); wl_surface_attach(surf->surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(surf->surf, 0, 0, buf->width, buf->height); wl_surface_commit(surf->surf); } static void render_csd_part(struct terminal *term, struct wl_surface *surf, struct buffer *buf, int width, int height, pixman_color_t *color) { xassert(term->window->csd_mode == CSD_YES); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], color, 1, &(pixman_rectangle16_t){0, 0, buf->width, buf->height}); } static void render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, struct fcft_font *font, struct buffer *buf, const char32_t *text, uint32_t _fg, uint32_t _bg, unsigned x) { pixman_region32_t clip; pixman_region32_init_rect(&clip, 0, 0, buf->width, buf->height); pixman_image_set_clip_region32(buf->pix[0], &clip); pixman_region32_fini(&clip); const bool gamma_correct = render_do_linear_blending(term); uint16_t alpha = _bg >> 24 | (_bg >> 24 << 8); pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &bg, 1, &(pixman_rectangle16_t){0, 0, buf->width, buf->height}); pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); const int x_ofs = term->font_x_ofs; const size_t len = c32len(text); struct fcft_text_run *text_run = NULL; const struct fcft_glyph **glyphs = NULL; const struct fcft_glyph *_glyphs[len]; size_t glyph_count = 0; if (fcft_capabilities() & FCFT_CAPABILITY_TEXT_RUN_SHAPING) { text_run = fcft_rasterize_text_run_utf32( font, len, (const char32_t *)text, term->font_subpixel); if (text_run != NULL) { glyphs = text_run->glyphs; glyph_count = text_run->count; } } if (glyphs == NULL) { for (size_t i = 0; i < len; i++) { const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( font, text[i], term->font_subpixel); if (glyph == NULL) continue; _glyphs[glyph_count++] = glyph; } glyphs = _glyphs; } pixman_image_t *src = pixman_image_create_solid_fill(&fg); /* Calculate baseline */ unsigned y; { const int line_height = buf->height; const int font_height = max(font->height, font->ascent + font->descent); const int glyph_top_y = round((line_height - font_height) / 2.); y = term->font_y_ofs + glyph_top_y + font->ascent; } for (size_t i = 0; i < glyph_count; i++) { const struct fcft_glyph *glyph = glyphs[i]; if (unlikely(glyph->is_color_glyph)) { pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, x + x_ofs + glyph->x, y - glyph->y, glyph->width, glyph->height); } else { pixman_image_composite32( PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, x + x_ofs + glyph->x, y - glyph->y, glyph->width, glyph->height); } x += glyph->advance.x; } fcft_text_run_destroy(text_run); pixman_image_unref(src); pixman_image_set_clip_region32(buf->pix[0], NULL); quirk_weston_subsurface_desync_on(sub_surf->sub); wayl_surface_scale(term->window, &sub_surf->surface, buf, term->scale); wl_surface_attach(sub_surf->surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(sub_surf->surface.surf, 0, 0, buf->width, buf->height); if (alpha == 0xffff) { struct wl_region *region = wl_compositor_create_region(term->wl->compositor); if (region != NULL) { wl_region_add(region, 0, 0, buf->width, buf->height); wl_surface_set_opaque_region(sub_surf->surface.surf, region); wl_region_destroy(region); } } else wl_surface_set_opaque_region(sub_surf->surface.surf, NULL); wl_surface_commit(sub_surf->surface.surf); quirk_weston_subsurface_desync_off(sub_surf->sub); } static void render_csd_title(struct terminal *term, const struct csd_data *info, struct buffer *buf) { xassert(term->window->csd_mode == CSD_YES); struct wayl_sub_surface *surf = &term->window->csd.surface[CSD_SURF_TITLE]; if (info->width == 0 || info->height == 0) return; uint32_t bg = term->conf->csd.color.title_set ? term->conf->csd.color.title : 0xffu << 24 | term->conf->colors.fg; uint32_t fg = term->conf->csd.color.buttons_set ? term->conf->csd.color.buttons : term->conf->colors.bg; if (!term->visual_focus) { bg = color_dim(term, bg); fg = color_dim(term, fg); } char32_t *_title_text = ambstoc32(term->window_title); const char32_t *title_text = _title_text != NULL ? _title_text : U""; struct wl_window *win = term->window; const struct fcft_glyph *M = fcft_rasterize_char_utf32( win->csd.font, U'M', term->font_subpixel); const int margin = M != NULL ? M->advance.x : win->csd.font->max_advance.x; render_osd(term, surf, win->csd.font, buf, title_text, fg, bg, margin); csd_commit(term, &surf->surface, buf); free(_title_text); } static void render_csd_border(struct terminal *term, enum csd_surface surf_idx, const struct csd_data *info, struct buffer *buf) { xassert(term->window->csd_mode == CSD_YES); xassert(surf_idx >= CSD_SURF_LEFT && surf_idx <= CSD_SURF_BOTTOM); struct wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; if (info->width == 0 || info->height == 0) return; const bool gamma_correct = render_do_linear_blending(term); { /* Fully transparent - no need to do a color space transform */ pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0, gamma_correct); render_csd_part(term, surf->surf, buf, info->width, info->height, &color); } /* * The "visible" border. */ float scale = term->scale; int bwidth = (int)roundf(term->conf->csd.border_width * scale); int vwidth = (int)roundf(term->conf->csd.border_width_visible * scale); /* Visible size */ xassert(bwidth >= vwidth); if (vwidth > 0) { const struct config *conf = term->conf; int x = 0, y = 0, w = 0, h = 0; switch (surf_idx) { case CSD_SURF_TOP: case CSD_SURF_BOTTOM: x = bwidth - vwidth; y = surf_idx == CSD_SURF_TOP ? info->height - vwidth : 0; w = info->width - 2 * x; h = vwidth; break; case CSD_SURF_LEFT: case CSD_SURF_RIGHT: x = surf_idx == CSD_SURF_LEFT ? bwidth - vwidth : 0; y = 0; w = vwidth; h = info->height; break; case CSD_SURF_TITLE: case CSD_SURF_MINIMIZE: case CSD_SURF_MAXIMIZE: case CSD_SURF_CLOSE: case CSD_SURF_COUNT: BUG("unexpected CSD surface type"); } xassert(x >= 0); xassert(y >= 0); xassert(w >= 0); xassert(h >= 0); xassert(x + w <= info->width); xassert(y + h <= info->height); uint32_t _color = conf->csd.color.border_set ? conf->csd.color.border : conf->csd.color.title_set ? conf->csd.color.title : 0xffu << 24 | term->conf->colors.fg; if (!term->visual_focus) _color = color_dim(term, _color); uint16_t alpha = _color >> 24 | (_color >> 24 << 8); pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 1, &(pixman_rectangle16_t){x, y, w, h}); } csd_commit(term, surf, buf); } static pixman_color_t get_csd_button_fg_color(const struct terminal *term) { const struct config *conf = term->conf; uint32_t _color = conf->colors.bg; uint16_t alpha = 0xffff; if (conf->csd.color.buttons_set) { _color = conf->csd.color.buttons; alpha = _color >> 24 | (_color >> 24 << 8); } return color_hex_to_pixman_with_alpha( _color, alpha, render_do_linear_blending(term)); } static void render_csd_button_minimize(struct terminal *term, struct buffer *buf) { pixman_color_t color = get_csd_button_fg_color(term); pixman_image_t *src = pixman_image_create_solid_fill(&color); const int max_height = buf->height / 3; const int max_width = buf->width / 3; int width = min(max_height, max_width); int thick = min(width / 2, 1 * term->scale); const int x_margin = (buf->width - width) / 2; const int y_margin = (buf->height - width) / 2; xassert(x_margin + width - thick >= 0); xassert(width - 2 * thick >= 0); xassert(y_margin + width - thick >= 0); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 1, (pixman_rectangle16_t[]) { {x_margin, y_margin + width - thick, width, thick} }); pixman_image_unref(src); } static void render_csd_button_maximize_maximized( struct terminal *term, struct buffer *buf) { pixman_color_t color = get_csd_button_fg_color(term); pixman_image_t *src = pixman_image_create_solid_fill(&color); const int max_height = buf->height / 3; const int max_width = buf->width / 3; int width = min(max_height, max_width); int thick = min(width / 2, 1 * term->scale); const int x_margin = (buf->width - width) / 2; const int y_margin = (buf->height - width) / 2; const int shrink = 1; xassert(x_margin + width - thick >= 0); xassert(width - 2 * thick >= 0); xassert(y_margin + width - thick >= 0); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 4, (pixman_rectangle16_t[]){ {x_margin + shrink, y_margin + shrink, width - 2 * shrink, thick}, { x_margin + shrink, y_margin + thick, thick, width - 2 * thick - shrink }, { x_margin + width - thick - shrink, y_margin + thick, thick, width - 2 * thick - shrink }, { x_margin + shrink, y_margin + width - thick - shrink, width - 2 * shrink, thick }}); pixman_image_unref(src); } static void render_csd_button_maximize_window( struct terminal *term, struct buffer *buf) { pixman_color_t color = get_csd_button_fg_color(term); pixman_image_t *src = pixman_image_create_solid_fill(&color); const int max_height = buf->height / 3; const int max_width = buf->width / 3; int width = min(max_height, max_width); int thick = min(width / 2, 1 * term->scale); const int x_margin = (buf->width - width) / 2; const int y_margin = (buf->height - width) / 2; xassert(x_margin + width - thick >= 0); xassert(width - 2 * thick >= 0); xassert(y_margin + width - thick >= 0); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 4, (pixman_rectangle16_t[]) { {x_margin, y_margin, width, thick}, { x_margin, y_margin + thick, thick, width - 2 * thick }, { x_margin + width - thick, y_margin + thick, thick, width - 2 * thick }, { x_margin, y_margin + width - thick, width, thick } }); pixman_image_unref(src); } static void render_csd_button_maximize(struct terminal *term, struct buffer *buf) { if (term->window->is_maximized) render_csd_button_maximize_maximized(term, buf); else render_csd_button_maximize_window(term, buf); } static void render_csd_button_close(struct terminal *term, struct buffer *buf) { pixman_color_t color = get_csd_button_fg_color(term); pixman_image_t *src = pixman_image_create_solid_fill(&color); const int max_height = buf->height / 3; const int max_width = buf->width / 3; int width = min(max_height, max_width); int thick = min(width / 2, 1 * term->scale); const int x_margin = (buf->width - width) / 2; const int y_margin = (buf->height - width) / 2; xassert(x_margin + width - thick >= 0); xassert(width - 2 * thick >= 0); xassert(y_margin + width - thick >= 0); pixman_triangle_t tri[4] = { { .p1 = { .x = pixman_int_to_fixed(x_margin), .y = pixman_int_to_fixed(y_margin + thick), }, .p2 = { .x = pixman_int_to_fixed(x_margin + width - thick), .y = pixman_int_to_fixed(y_margin + width), }, .p3 = { .x = pixman_int_to_fixed(x_margin + thick), .y = pixman_int_to_fixed(y_margin), }, }, { .p1 = { .x = pixman_int_to_fixed(x_margin + width), .y = pixman_int_to_fixed(y_margin + width - thick), }, .p2 = { .x = pixman_int_to_fixed(x_margin + thick), .y = pixman_int_to_fixed(y_margin), }, .p3 = { .x = pixman_int_to_fixed(x_margin + width - thick), .y = pixman_int_to_fixed(y_margin + width), }, }, { .p1 = { .x = pixman_int_to_fixed(x_margin), .y = pixman_int_to_fixed(y_margin + width - thick), }, .p2 = { .x = pixman_int_to_fixed(x_margin + width), .y = pixman_int_to_fixed(y_margin + thick), }, .p3 = { .x = pixman_int_to_fixed(x_margin + thick), .y = pixman_int_to_fixed(y_margin + width), }, }, { .p1 = { .x = pixman_int_to_fixed(x_margin + width), .y = pixman_int_to_fixed(y_margin + thick), }, .p2 = { .x = pixman_int_to_fixed(x_margin), .y = pixman_int_to_fixed(y_margin + width - thick), }, .p3 = { .x = pixman_int_to_fixed(x_margin + width - thick), .y = pixman_int_to_fixed(y_margin), }, }, }; pixman_composite_triangles( PIXMAN_OP_OVER, src, buf->pix[0], PIXMAN_a1, 0, 0, 0, 0, 4, tri); pixman_image_unref(src); } static bool any_pointer_is_on_button(const struct terminal *term, enum csd_surface csd_surface) { if (unlikely(tll_length(term->wl->seats) == 0)) return false; tll_foreach(term->wl->seats, it) { const struct seat *seat = &it->item; if (seat->mouse.x < 0) continue; if (seat->mouse.y < 0) continue; struct csd_data info = get_csd_data(term, csd_surface); if (seat->mouse.x > info.width) continue; if (seat->mouse.y > info.height) continue; return true; } return false; } static void render_csd_button(struct terminal *term, enum csd_surface surf_idx, const struct csd_data *info, struct buffer *buf) { xassert(term->window->csd_mode == CSD_YES); xassert(surf_idx >= CSD_SURF_MINIMIZE && surf_idx <= CSD_SURF_CLOSE); struct wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; if (info->width == 0 || info->height == 0) return; uint32_t _color; uint16_t alpha = 0xffff; bool is_active = false; bool is_set = false; const uint32_t *conf_color = NULL; switch (surf_idx) { case CSD_SURF_MINIMIZE: _color = term->conf->colors.table[4]; /* blue */ is_set = term->conf->csd.color.minimize_set; conf_color = &term->conf->csd.color.minimize; is_active = term->active_surface == TERM_SURF_BUTTON_MINIMIZE && any_pointer_is_on_button(term, CSD_SURF_MINIMIZE); break; case CSD_SURF_MAXIMIZE: _color = term->conf->colors.table[2]; /* green */ is_set = term->conf->csd.color.maximize_set; conf_color = &term->conf->csd.color.maximize; is_active = term->active_surface == TERM_SURF_BUTTON_MAXIMIZE && any_pointer_is_on_button(term, CSD_SURF_MAXIMIZE); break; case CSD_SURF_CLOSE: _color = term->conf->colors.table[1]; /* red */ is_set = term->conf->csd.color.close_set; conf_color = &term->conf->csd.color.quit; is_active = term->active_surface == TERM_SURF_BUTTON_CLOSE && any_pointer_is_on_button(term, CSD_SURF_CLOSE); break; default: BUG("unhandled surface type: %u", (unsigned)surf_idx); break; } if (is_active) { if (is_set) { _color = *conf_color; alpha = _color >> 24 | (_color >> 24 << 8); } } else { _color = 0; alpha = 0; } if (!term->visual_focus) _color = color_dim(term, _color); const bool gamma_correct = render_do_linear_blending(term); pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); render_csd_part(term, surf->surf, buf, info->width, info->height, &color); switch (surf_idx) { case CSD_SURF_MINIMIZE: render_csd_button_minimize(term, buf); break; case CSD_SURF_MAXIMIZE: render_csd_button_maximize(term, buf); break; case CSD_SURF_CLOSE: render_csd_button_close(term, buf); break; default: BUG("unhandled surface type: %u", (unsigned)surf_idx); break; } csd_commit(term, surf, buf); } static void render_csd(struct terminal *term) { xassert(term->window->csd_mode == CSD_YES); if (term->window->is_fullscreen) return; const float scale = term->scale; struct csd_data infos[CSD_SURF_COUNT]; int widths[CSD_SURF_COUNT]; int heights[CSD_SURF_COUNT]; for (size_t i = 0; i < CSD_SURF_COUNT; i++) { infos[i] = get_csd_data(term, i); const int x = infos[i].x; const int y = infos[i].y; const int width = infos[i].width; const int height = infos[i].height; struct wl_surface *surf = term->window->csd.surface[i].surface.surf; struct wl_subsurface *sub = term->window->csd.surface[i].sub; xassert(surf != NULL); xassert(sub != NULL); if (width == 0 || height == 0) { widths[i] = heights[i] = 0; wl_subsurface_set_position(sub, 0, 0); wl_surface_attach(surf, NULL, 0, 0); wl_surface_commit(surf); continue; } widths[i] = width; heights[i] = height; wl_subsurface_set_position(sub, roundf(x / scale), roundf(y / scale)); } struct buffer *bufs[CSD_SURF_COUNT]; shm_get_many(term->render.chains.csd, CSD_SURF_COUNT, widths, heights, bufs, true); for (size_t i = CSD_SURF_LEFT; i <= CSD_SURF_BOTTOM; i++) render_csd_border(term, i, &infos[i], bufs[i]); for (size_t i = CSD_SURF_MINIMIZE; i <= CSD_SURF_CLOSE; i++) render_csd_button(term, i, &infos[i], bufs[i]); render_csd_title(term, &infos[CSD_SURF_TITLE], bufs[CSD_SURF_TITLE]); } static void render_scrollback_position(struct terminal *term) { if (term->conf->scrollback.indicator.position == SCROLLBACK_INDICATOR_POSITION_NONE) return; struct wl_window *win = term->window; if (term->grid->view == term->grid->offset) { if (win->scrollback_indicator.surface.surf != NULL) { wayl_win_subsurface_destroy(&win->scrollback_indicator); /* Work around Sway bug - unmapping a sub-surface does not damage * the underlying surface */ quirk_sway_subsurface_unmap(term); } return; } if (win->scrollback_indicator.surface.surf == NULL) { if (!wayl_win_subsurface_new( win, &win->scrollback_indicator, false)) { LOG_ERR("failed to create scrollback indicator surface"); return; } } xassert(win->scrollback_indicator.surface.surf != NULL); xassert(win->scrollback_indicator.sub != NULL); /* Find absolute row number of the scrollback start */ int scrollback_start = term->grid->offset + term->rows; int empty_rows = 0; while (term->grid->rows[scrollback_start & (term->grid->num_rows - 1)] == NULL) { scrollback_start++; empty_rows++; } /* Rebase viewport against scrollback start (so that 0 is at * the beginning of the scrollback) */ int rebased_view = term->grid->view - scrollback_start + term->grid->num_rows; rebased_view &= term->grid->num_rows - 1; /* How much of the scrollback is actually used? */ int populated_rows = term->grid->num_rows - empty_rows; xassert(populated_rows > 0); xassert(populated_rows <= term->grid->num_rows); /* * How far down in the scrollback we are. * * 0% -> at the beginning of the scrollback * 100% -> at the bottom, i.e. where new lines are inserted */ double percent = rebased_view + term->rows == populated_rows ? 1.0 : (double)rebased_view / (populated_rows - term->rows); char32_t _text[64]; const char32_t *text = _text; int cell_count = 0; /* *What* to render */ switch (term->conf->scrollback.indicator.format) { case SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE: { char percent_str[8]; snprintf(percent_str, sizeof(percent_str), "%u%%", (int)(100 * percent)); mbstoc32(_text, percent_str, ALEN(_text)); cell_count = 3; break; } case SCROLLBACK_INDICATOR_FORMAT_LINENO: { char lineno_str[64]; snprintf(lineno_str, sizeof(lineno_str), "%d", rebased_view + 1); mbstoc32(_text, lineno_str, ALEN(_text)); cell_count = (int)ceilf(log10f(term->grid->num_rows)); break; } case SCROLLBACK_INDICATOR_FORMAT_TEXT: text = term->conf->scrollback.indicator.text; cell_count = c32len(text); break; } const float scale = term->scale; const int margin = (int)roundf(3. * scale); int width = margin + cell_count * term->cell_width + margin; int height = margin + term->cell_height + margin; width = roundf(scale * ceilf(width / scale)); height = roundf(scale * ceilf(height / scale)); /* *Where* to render - parent relative coordinates */ int surf_top = 0; switch (term->conf->scrollback.indicator.position) { case SCROLLBACK_INDICATOR_POSITION_NONE: BUG("Invalid scrollback indicator position type"); return; case SCROLLBACK_INDICATOR_POSITION_FIXED: surf_top = term->cell_height - margin; break; case SCROLLBACK_INDICATOR_POSITION_RELATIVE: { int lines = term->rows - 2; /* Avoid using first and last rows */ if (term->is_searching) { /* Make sure we don't collide with the scrollback search box */ lines--; } lines = max(lines, 0); int pixels = max(lines * term->cell_height - height + 2 * margin, 0); surf_top = term->cell_height - margin + (int)(percent * pixels); break; } } int x = term->width - margin - width; int y = term->margins.top + surf_top; x = roundf(scale * ceilf(x / scale)); y = roundf(scale * ceilf(y / scale)); if (y + height > term->height) { wl_surface_attach(win->scrollback_indicator.surface.surf, NULL, 0, 0); wl_surface_commit(win->scrollback_indicator.surface.surf); return; } struct buffer_chain *chain = term->render.chains.scrollback_indicator; struct buffer *buf = shm_get_buffer(chain, width, height, false); wl_subsurface_set_position( win->scrollback_indicator.sub, roundf(x / scale), roundf(y / scale)); uint32_t fg = term->colors.table[0]; uint32_t bg = term->colors.table[8 + 4]; if (term->conf->colors.use_custom.scrollback_indicator) { fg = term->conf->colors.scrollback_indicator.fg; bg = term->conf->colors.scrollback_indicator.bg; } render_osd( term, &win->scrollback_indicator, term->fonts[0], buf, text, fg, 0xffu << 24 | bg, width - margin - c32len(text) * term->cell_width); } static void render_render_timer(struct terminal *term, struct timespec render_time) { struct wl_window *win = term->window; char usecs_str[256]; double usecs = render_time.tv_sec * 1000000 + render_time.tv_nsec / 1000.0; snprintf(usecs_str, sizeof(usecs_str), "%.2f µs", usecs); char32_t text[256]; mbstoc32(text, usecs_str, ALEN(text)); const float scale = term->scale; const int cell_count = c32len(text); const int margin = (int)roundf(3. * scale); int width = margin + cell_count * term->cell_width + margin; int height = margin + term->cell_height + margin; width = roundf(scale * ceilf(width / scale)); height = roundf(scale * ceilf(height / scale)); struct buffer_chain *chain = term->render.chains.render_timer; struct buffer *buf = shm_get_buffer(chain, width, height, false); wl_subsurface_set_position( win->render_timer.sub, roundf(margin / scale), roundf((term->margins.top + term->cell_height - margin) / scale)); render_osd( term, &win->render_timer, term->fonts[0], buf, text, term->colors.table[0], 0xffu << 24 | term->colors.table[8 + 1], margin); } static void frame_callback( void *data, struct wl_callback *wl_callback, uint32_t callback_data); static const struct wl_callback_listener frame_listener = { .done = &frame_callback, }; static void force_full_repaint(struct terminal *term, struct buffer *buf) { tll_free(term->grid->scroll_damage); render_margin(term, buf, 0, term->rows, true); term_damage_view(term); } static void reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old) { 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; } pixman_region32_t dirty; pixman_region32_init(&dirty); /* * Figure out current frame's damage region * * If current frame doesn't have any scroll damage, we can simply * subtract this frame's damage from the last frame's damage. That * way, we don't have to copy areas from the old frame that'll * just get overwritten by current frame. * * Note that this is row based. A "half damaged" row is not * excluded. I.e. the entire row will be copied from the old frame * to the new, and then when actually rendering the new frame, the * updated cells will overwrite parts of the copied row. * * Since we're scanning the entire viewport anyway, we also track * whether *all* cells are to be updated. In this case, just force * a full re-rendering, and don't copy anything from the old * frame. */ bool full_repaint_needed = true; for (int r = 0; r < term->rows; r++) { const struct row *row = grid_row_in_view(term->grid, r); if (!row->dirty) { full_repaint_needed = false; continue; } bool row_all_dirty = true; for (int c = 0; c < term->cols; c++) { if (row->cells[c].attrs.clean) { row_all_dirty = false; full_repaint_needed = false; break; } } if (row_all_dirty) { pixman_region32_union_rect( &dirty, &dirty, term->margins.left, term->margins.top + r * term->cell_height, term->width - term->margins.left - term->margins.right, term->cell_height); } } if (full_repaint_needed) { force_full_repaint(term, new); return; } /* * TODO: re-apply last frame's scroll damage * * We used to do this, but it turned out to be buggy. If we decide * to re-add it, this is where to do it. Note that we'd also have * to remove the updates to buf->dirty from grid_render_scroll() * and grid_render_scroll_reverse(). */ if (tll_length(term->grid->scroll_damage) == 0) { /* * We can only subtract current frame's damage from the old * frame's if we don't have any scroll damage. * * If we do have scroll damage, the damage region we * calculated above is not (yet) valid - we need to apply the * current frame's scroll damage *first*. This is done later, * when rendering the frame. */ pixman_region32_subtract(&dirty, &old->dirty[0], &dirty); pixman_image_set_clip_region32(new->pix[0], &dirty); } else { /* Copy *all* of last frame's damaged areas */ pixman_image_set_clip_region32(new->pix[0], &old->dirty[0]); } pixman_image_composite32( PIXMAN_OP_SRC, old->pix[0], NULL, new->pix[0], 0, 0, 0, 0, 0, 0, term->width, term->height); pixman_image_set_clip_region32(new->pix[0], NULL); pixman_region32_fini(&dirty); } static void dirty_old_cursor(struct terminal *term) { if (term->render.last_cursor.row != NULL && !term->render.last_cursor.hidden) { struct row *row = term->render.last_cursor.row; struct cell *cell = &row->cells[term->render.last_cursor.col]; cell->attrs.clean = 0; row->dirty = true; } /* Remember current cursor position, for the next frame */ term->render.last_cursor.row = grid_row(term->grid, term->grid->cursor.point.row); term->render.last_cursor.col = term->grid->cursor.point.col; term->render.last_cursor.hidden = term->hide_cursor; } static void dirty_cursor(struct terminal *term) { if (term->hide_cursor) return; const struct coord *cursor = &term->grid->cursor.point; struct row *row = grid_row(term->grid, cursor->row); struct cell *cell = &row->cells[cursor->col]; cell->attrs.clean = 0; row->dirty = true; } static void grid_render(struct terminal *term) { if (term->shutdown.in_progress) return; struct timespec start_time, start_double_buffering = {0}, stop_double_buffering = {0}; if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) clock_gettime(CLOCK_MONOTONIC, &start_time); xassert(term->width > 0); xassert(term->height > 0); struct buffer_chain *chain = term->render.chains.grid; bool use_alpha = !term->window->is_fullscreen && term->colors.alpha != 0xffff; struct buffer *buf = shm_get_buffer( chain, term->width, term->height, use_alpha); /* Dirty old and current cursor cell, to ensure they're repainted */ dirty_old_cursor(term); dirty_cursor(term); if (term->render.last_buf == NULL || term->render.last_buf->width != buf->width || term->render.last_buf->height != buf->height || term->render.margins) { force_full_repaint(term, buf); } else if (buf->age > 0) { LOG_DBG("buffer age: %u (%p)", buf->age, (void *)buf); xassert(term->render.last_buf != NULL); xassert(term->render.last_buf != buf); xassert(term->render.last_buf->width == buf->width); xassert(term->render.last_buf->height == buf->height); clock_gettime(CLOCK_MONOTONIC, &start_double_buffering); reapply_old_damage(term, buf, term->render.last_buf); clock_gettime(CLOCK_MONOTONIC, &stop_double_buffering); } if (term->render.last_buf != NULL) { shm_unref(term->render.last_buf); term->render.last_buf = NULL; } term->render.last_buf = buf; shm_addref(buf); buf->age = 0; tll_foreach(term->grid->scroll_damage, it) { switch (it->item.type) { case DAMAGE_SCROLL: if (term->grid->view == term->grid->offset) grid_render_scroll(term, buf, &it->item); break; case DAMAGE_SCROLL_REVERSE: if (term->grid->view == term->grid->offset) grid_render_scroll_reverse(term, buf, &it->item); break; case DAMAGE_SCROLL_IN_VIEW: grid_render_scroll(term, buf, &it->item); break; case DAMAGE_SCROLL_REVERSE_IN_VIEW: grid_render_scroll_reverse(term, buf, &it->item); break; } tll_remove(term->grid->scroll_damage, it); } /* * Ensure selected cells have their 'selected' bit set. This is * normally "automatically" true - the bit is set when the * selection is made. * * However, if the cell is updated (printed to) while the * selection is active, the 'selected' bit is cleared. Checking * for this and re-setting the bit in term_print() is too * expensive performance wise. * * Instead, we synchronize the selection bits here and now. This * makes the performance impact linear to the number of selected * cells rather than to the number of updated cells. * * (note that selection_dirty_cells() will not set the dirty flag * on cells where the 'selected' bit is already set) */ selection_dirty_cells(term); /* Translate offset-relative row to view-relative, unless cursor * is hidden, then we just set it to -1 */ struct coord cursor = {-1, -1}; if (!term->hide_cursor) { cursor = term->grid->cursor.point; cursor.row += term->grid->offset; cursor.row -= term->grid->view; cursor.row &= term->grid->num_rows - 1; } if (term->conf->tweak.overflowing_glyphs) { /* * Pre-pass to dirty cells affected by overflowing glyphs. * * Given any two pair of cells where the first cell is * overflowing into the second, *both* cells must be * re-rendered if any one of them is dirty. * * Thus, given a string of overflowing glyphs, with a single * dirty cell in the middle, we need to re-render the entire * string. */ for (int r = 0; r < term->rows; r++) { struct row *row = grid_row_in_view(term->grid, r); if (!row->dirty) continue; /* Loop row from left to right, looking for dirty cells */ for (struct cell *cell = &row->cells[0]; cell < &row->cells[term->cols]; cell++) { if (cell->attrs.clean) continue; /* * Cell is dirty, go back and dirty previous cells, if * they are overflowing. * * As soon as we see a non-overflowing cell we can * stop, since it isn't affecting the string of * overflowing glyphs that follows it. * * As soon as we see a dirty cell, we can stop, since * that means we've already handled it (remember the * outer loop goes from left to right). */ for (struct cell *c = cell - 1; c >= &row->cells[0]; c--) { if (c->attrs.confined) break; if (!c->attrs.clean) break; c->attrs.clean = false; } /* * Now move forward, dirtying all cells until we hit a * non-overflowing cell. * * Note that the first non-overflowing cell must be * re-rendered as well, but any cell *after* that is * unaffected by the string of overflowing glyphs * we're dealing with right now. * * For performance, this iterates the *outer* loop's * cell pointer - no point in re-checking all these * glyphs again, in the outer loop. */ for (; cell < &row->cells[term->cols]; cell++) { cell->attrs.clean = false; if (cell->attrs.confined) break; } } } } #if defined(_DEBUG) for (int r = 0; r < term->rows; r++) { const struct row *row = grid_row_in_view(term->grid, r); if (row->dirty) { bool all_clean = true; for (int c = 0; c < term->cols; c++) { if (!row->cells[c].attrs.clean) { all_clean = false; break; } } if (all_clean) BUG("row #%d is dirty, but all cells are marked as clean", r); } else { for (int c = 0; c < term->cols; c++) { if (!row->cells[c].attrs.clean) BUG("row #%d is clean, but cell #%d is dirty", r, c); } } } #endif pixman_region32_t damage; pixman_region32_init(&damage); render_sixel_images(term, buf->pix[0], &damage, &cursor); if (term->render.workers.count > 0) { mtx_lock(&term->render.workers.lock); term->render.workers.buf = buf; for (size_t i = 0; i < term->render.workers.count; i++) sem_post(&term->render.workers.start); xassert(tll_length(term->render.workers.queue) == 0); } for (int r = 0; r < term->rows; r++) { struct row *row = grid_row_in_view(term->grid, r); if (!row->dirty) continue; row->dirty = false; if (term->render.workers.count > 0) tll_push_back(term->render.workers.queue, r); else { /* TODO: damage region */ int cursor_col = cursor.row == r ? cursor.col : -1; render_row(term, buf->pix[0], &damage, row, r, cursor_col); } } /* Signal workers the frame is done */ if (term->render.workers.count > 0) { for (size_t i = 0; i < term->render.workers.count; i++) tll_push_back(term->render.workers.queue, -1); mtx_unlock(&term->render.workers.lock); for (size_t i = 0; i < term->render.workers.count; i++) sem_wait(&term->render.workers.done); term->render.workers.buf = NULL; } for (size_t i = 0; i < term->render.workers.count; i++) pixman_region32_union(&damage, &damage, &buf->dirty[i + 1]); pixman_region32_union(&buf->dirty[0], &buf->dirty[0], &damage); { int box_count = 0; pixman_box32_t *boxes = pixman_region32_rectangles(&damage, &box_count); for (size_t i = 0; i < box_count; i++) { wl_surface_damage_buffer( term->window->surface.surf, boxes[i].x1, boxes[i].y1, boxes[i].x2 - boxes[i].x1, boxes[i].y2 - boxes[i].y1); } } pixman_region32_fini(&damage); render_overlay(term); render_ime_preedit(term, buf); render_scrollback_position(term); if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) { struct timespec end_time; clock_gettime(CLOCK_MONOTONIC, &end_time); struct timespec 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 total_render_time; timespec_add(&render_time, &double_buffering_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)", (long)total_render_time.tv_sec, total_render_time.tv_nsec, (long)render_time.tv_sec, render_time.tv_nsec, (long)double_buffering_time.tv_sec, double_buffering_time.tv_nsec); break; case RENDER_TIMER_OSD: case RENDER_TIMER_NONE: break; } switch (term->conf->tweak.render_timer) { case RENDER_TIMER_OSD: case RENDER_TIMER_BOTH: render_render_timer(term, total_render_time); break; case RENDER_TIMER_LOG: case RENDER_TIMER_NONE: break; } } xassert(term->grid->offset >= 0 && term->grid->offset < term->grid->num_rows); xassert(term->grid->view >= 0 && term->grid->view < term->grid->num_rows); xassert(term->window->frame_callback == NULL); term->window->frame_callback = wl_surface_frame(term->window->surface.surf); wl_callback_add_listener(term->window->frame_callback, &frame_listener, term); wayl_win_scale(term->window, buf); if (term->wl->presentation != NULL && term->conf->presentation_timings) { struct timespec commit_time; clock_gettime(term->wl->presentation_clock_id, &commit_time); struct wp_presentation_feedback *feedback = wp_presentation_feedback( term->wl->presentation, term->window->surface.surf); if (feedback == NULL) { LOG_WARN("failed to create presentation feedback"); } else { struct presentation_context *ctx = xmalloc(sizeof(*ctx)); *ctx = (struct presentation_context){ .term = term, .input.tv_sec = term->render.input_time.tv_sec, .input.tv_usec = term->render.input_time.tv_nsec / 1000, .commit.tv_sec = commit_time.tv_sec, .commit.tv_usec = commit_time.tv_nsec / 1000, }; wp_presentation_feedback_add_listener( feedback, &presentation_feedback_listener, ctx); term->render.input_time.tv_sec = 0; term->render.input_time.tv_nsec = 0; } } if (term->conf->tweak.damage_whole_window) { wl_surface_damage_buffer( term->window->surface.surf, 0, 0, INT32_MAX, INT32_MAX); } wl_surface_attach(term->window->surface.surf, buf->wl_buf, 0, 0); wl_surface_commit(term->window->surface.surf); } static void render_search_box(struct terminal *term) { xassert(term->window->search.sub != NULL); /* * We treat the search box pretty much like a row of cells. That * is, a glyph is either 1 or 2 (or more) "cells" wide. * * The search 'length', and 'cursor' (position) is in * *characters*, not cells. This means we need to translate from * character count to cell count when calculating the length of * the search box, where in the search string we should start * rendering etc. */ #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED /* TODO: do we want to/need to handle multi-seat? */ struct seat *ime_seat = NULL; tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) { ime_seat = &it->item; break; } } size_t text_len = term->search.len; if (ime_seat != NULL && ime_seat->ime.preedit.text != NULL) text_len += c32len(ime_seat->ime.preedit.text); char32_t *text = xmalloc((text_len + 1) * sizeof(char32_t)); text[0] = U'\0'; /* Copy everything up to the cursor */ c32ncpy(text, term->search.buf, term->search.cursor); text[term->search.cursor] = U'\0'; /* Insert pre-edit text at cursor */ if (ime_seat != NULL && ime_seat->ime.preedit.text != NULL) c32cat(text, ime_seat->ime.preedit.text); /* And finally everything after the cursor */ c32ncat(text, &term->search.buf[term->search.cursor], term->search.len - term->search.cursor); #else const char32_t *text = term->search.buf; const size_t text_len = term->search.len; #endif /* Calculate the width of each character */ int widths[text_len + 1]; for (size_t i = 0; i < text_len; i++) widths[i] = max(0, c32width(text[i])); widths[text_len] = 0; const size_t total_cells = c32swidth(text, text_len); const size_t wanted_visible_cells = max(20, total_cells); const float scale = term->scale; xassert(scale >= 1.); const size_t margin = (size_t)roundf(3 * scale); size_t width = term->width - 2 * margin; size_t height = min( term->height - 2 * margin, 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; struct buffer_chain *chain = term->render.chains.search; struct buffer *buf = shm_get_buffer(chain, width, height, true); pixman_region32_t clip; pixman_region32_init_rect(&clip, 0, 0, width, height); pixman_image_set_clip_region32(buf->pix[0], &clip); pixman_region32_fini(&clip); #define WINDOW_X(x) (margin + x) #define WINDOW_Y(y) (term->height - margin - height + y) 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; /* Background - yellow on empty/match, red on mismatch (default) */ const bool gamma_correct = render_do_linear_blending(term); const pixman_color_t color = color_hex_to_pixman( is_match ? (custom_colors ? term->conf->colors.search_box.match.bg : term->colors.table[3]) : (custom_colors ? term->conf->colors.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, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &transparent, 1, &(pixman_rectangle16_t){0, 0, width - visible_width, height}); struct fcft_font *font = term->fonts[0]; const int x_left = width - visible_width + margin; 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], gamma_correct); /* Move offset we start rendering at, to ensure the cursor is visible */ for (size_t i = 0, cell_idx = 0; i <= term->search.cursor; cell_idx += widths[i], i++) { if (i != term->search.cursor) continue; #if (FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (ime_seat != NULL && ime_seat->ime.preedit.cells != NULL) { if (ime_seat->ime.preedit.cursor.start == ime_seat->ime.preedit.cursor.end) { /* All IME's I've seen so far keeps the cursor at * index 0, so ensure the *end* of the pre-edit string * is visible */ cell_idx += ime_seat->ime.preedit.count; } else { /* Try to predict in which direction we'll shift the text */ if (cell_idx + ime_seat->ime.preedit.cursor.start > glyph_offset) cell_idx += ime_seat->ime.preedit.cursor.end; else cell_idx += ime_seat->ime.preedit.cursor.start; } } #endif if (cell_idx < glyph_offset) { /* Shift to the *left*, making *this* character the * *first* visible one */ term->render.search_glyph_offset = glyph_offset = cell_idx; } else if (cell_idx > glyph_offset + visible_cells) { /* Shift to the *right*, making *this* character the * *last* visible one */ term->render.search_glyph_offset = glyph_offset = cell_idx - min(cell_idx, visible_cells); } /* Adjust offset if there is free space available */ if (total_cells - glyph_offset < visible_cells) { term->render.search_glyph_offset = glyph_offset = total_cells - min(total_cells, visible_cells); } break; } /* Ensure offset is at a character boundary */ for (size_t i = 0, cell_idx = 0; i <= text_len; cell_idx += widths[i], i++) { if (cell_idx >= glyph_offset) { term->render.search_glyph_offset = glyph_offset = cell_idx; break; } } /* * Render the search string, starting at 'glyph_offset'. Note that * glyph_offset is in cells, not characters */ for (size_t i = 0, cell_idx = 0, width = widths[i], next_cell_idx = width; i < text_len; i++, cell_idx = next_cell_idx, width = widths[i], next_cell_idx += width) { /* Convert subsurface coordinates to window coordinates*/ /* Render cursor */ if (i == term->search.cursor) { #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED bool have_preedit = ime_seat != NULL && ime_seat->ime.preedit.cells != NULL; bool hidden = ime_seat != NULL && ime_seat->ime.preedit.cursor.hidden; if (have_preedit && !hidden) { /* Cursor may be outside the visible area: * cell_idx-glyph_offset can be negative */ int cells_left = visible_cells - max( (ssize_t)(cell_idx - glyph_offset), 0); /* If cursor is outside the visible area, we need to * adjust our rectangle's position */ int start = ime_seat->ime.preedit.cursor.start + min((ssize_t)(cell_idx - glyph_offset), 0); int end = ime_seat->ime.preedit.cursor.end + min((ssize_t)(cell_idx - glyph_offset), 0); if (start == end) { int count = min(ime_seat->ime.preedit.count, cells_left); /* Underline the entire (visible part of) pre-edit text */ draw_underline(term, buf->pix[0], font, &fg, x, y, count); /* Bar-styled cursor, if in the visible area */ if (start >= 0 && start <= visible_cells) { draw_beam_cursor( term, buf->pix[0], font, &fg, x + start * term->cell_width, y); } term_ime_set_cursor_rect(term, WINDOW_X(x + start * term->cell_width), WINDOW_Y(y), 1, term->cell_height); } else { /* Underline everything before and after the cursor */ int count1 = min(start, cells_left); int count2 = max( min(ime_seat->ime.preedit.count - ime_seat->ime.preedit.cursor.end, cells_left - end), 0); draw_underline(term, buf->pix[0], font, &fg, x, y, count1); draw_underline(term, buf->pix[0], font, &fg, x + end * term->cell_width, y, count2); /* TODO: how do we handle a partially hidden rectangle? */ if (start >= 0 && end <= visible_cells) { draw_hollow_block( term, buf->pix[0], &fg, x + start * term->cell_width, y, end - start); } term_ime_set_cursor_rect(term, WINDOW_X(x + start * term->cell_width), WINDOW_Y(y), term->cell_width * (end - start), term->cell_height); } } else if (!have_preedit) #endif { /* Cursor *should* be in the visible area */ xassert(cell_idx >= glyph_offset); xassert(cell_idx <= glyph_offset + visible_cells); draw_beam_cursor(term, buf->pix[0], font, &fg, x, y); term_ime_set_cursor_rect( term, WINDOW_X(x), WINDOW_Y(y), 1, term->cell_height); } } if (next_cell_idx >= glyph_offset && next_cell_idx - glyph_offset > visible_cells) { /* We're now beyond the visible area - nothing more to render */ break; } if (cell_idx < glyph_offset) { /* We haven't yet reached the visible part of the string */ cell_idx = next_cell_idx; continue; } const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( font, text[i], term->font_subpixel); if (glyph == NULL) { cell_idx = next_cell_idx; continue; } if (unlikely(glyph->is_color_glyph)) { pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, x + x_ofs + glyph->x, y + term->font_baseline - glyph->y, glyph->width, glyph->height); } else { int combining_ofs = width == 0 ? (glyph->x < 0 ? width * term->cell_width : (width - 1) * term->cell_width) : 0; /* Not a zero-width character - no additional offset */ pixman_image_t *src = pixman_image_create_solid_fill(&fg); pixman_image_composite32( PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, x + x_ofs + combining_ofs + glyph->x, y + term->font_baseline - glyph->y, glyph->width, glyph->height); pixman_image_unref(src); } x += width * term->cell_width; cell_idx = next_cell_idx; } #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (ime_seat != NULL && ime_seat->ime.preedit.cells != NULL) /* Already rendered */; else #endif if (term->search.cursor >= term->search.len) { draw_beam_cursor(term, buf->pix[0], font, &fg, x, y); term_ime_set_cursor_rect( term, WINDOW_X(x), WINDOW_Y(y), 1, term->cell_height); } quirk_weston_subsurface_desync_on(term->window->search.sub); /* TODO: this is only necessary on a window resize */ wl_subsurface_set_position( term->window->search.sub, roundf(margin / scale), roundf(max(0, (int32_t)term->height - height - margin) / scale)); wayl_surface_scale(term->window, &term->window->search.surface, buf, scale); wl_surface_attach(term->window->search.surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(term->window->search.surface.surf, 0, 0, width, height); struct wl_region *region = wl_compositor_create_region(term->wl->compositor); if (region != NULL) { wl_region_add(region, width - visible_width, 0, visible_width, height); wl_surface_set_opaque_region(term->window->search.surface.surf, region); wl_region_destroy(region); } wl_surface_commit(term->window->search.surface.surf); quirk_weston_subsurface_desync_off(term->window->search.sub); #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED free(text); #endif #undef WINDOW_X #undef WINDOW_Y } static void render_urls(struct terminal *term) { struct wl_window *win = term->window; xassert(tll_length(win->urls) > 0); const float scale = term->scale; const int x_margin = (int)roundf(2 * scale); const int y_margin = (int)roundf(1 * scale); /* Calculate view start, counted from the *current* scrollback start */ const int scrollback_end = (term->grid->offset + term->rows) & (term->grid->num_rows - 1); const int view_start = (term->grid->view - scrollback_end + term->grid->num_rows) & (term->grid->num_rows - 1); const int view_end = view_start + term->rows - 1; const bool show_url = term->urls_show_uri_on_jump_label; /* * There can potentially be a lot of URLs. * * Since each URL is a separate sub-surface, and requires its own * SHM buffer, we may be allocating a lot of buffers. * * SHM buffers normally have their own, private SHM buffer * pool. Each pool is mmapped, and thus allocates *at least* * 4K. Since URL labels are typically small, we end up using an * excessive amount of both virtual and physical memory. * * For this reason, we instead use shm_get_many(), which uses a * single, shared pool for all buffers. * * To be able to use it, we need to have all the *all* the buffer * dimensions up front. * * Thus, the first iteration through the URLs do the heavy * lifting: builds the label contents and calculates both its * position and size. But instead of rendering the label * immediately, we store the calculated data, and then do a second * pass, where we first get all our buffers, and then render to * them. */ /* Positioning data + label contents */ struct { const struct wl_url *url; char32_t *text; int x; int y; } info[tll_length(win->urls)]; /* For shm_get_many() */ int widths[tll_length(win->urls)]; int heights[tll_length(win->urls)]; size_t render_count = 0; tll_foreach(win->urls, it) { const struct url *url = it->item.url; const char32_t *key = url->key; const size_t entered_key_len = c32len(term->url_keys); if (key == NULL) { /* TODO: if we decide to use the .text field, we cannot * just skip the entire jump label like this */ continue; } struct wl_surface *surf = it->item.surf.surface.surf; struct wl_subsurface *sub_surf = it->item.surf.sub; if (surf == NULL || sub_surf == NULL) continue; bool hide = false; const struct coord *pos = &url->range.start; const int _row = (pos->row - scrollback_end + term->grid->num_rows) & (term->grid->num_rows - 1); if (_row < view_start || _row > view_end) hide = true; if (c32len(key) <= entered_key_len) hide = true; if (c32ncasecmp(term->url_keys, key, entered_key_len) != 0) hide = true; if (hide) { wl_surface_attach(surf, NULL, 0, 0); wl_surface_commit(surf); continue; } int col = pos->col; int row = pos->row - term->grid->view; while (row < 0) row += term->grid->num_rows; row &= (term->grid->num_rows - 1); /* Position label slightly above and to the left */ int x = col * term->cell_width - 15 * term->cell_width / 10; int y = row * term->cell_height - 5 * term->cell_height / 10; /* Don't position it outside our window */ if (x < -term->margins.left) x = -term->margins.left; if (y < -term->margins.top) y = -term->margins.top; /* Maximum width of label, in pixels */ const int max_width = term->width - term->margins.left - term->margins.right - x; const int max_cols = max_width / term->cell_width; const size_t key_len = c32len(key); size_t url_len = mbstoc32(NULL, url->url, 0); if (url_len == (size_t)-1) url_len = 0; char32_t url_wchars[url_len + 1]; mbstoc32(url_wchars, url->url, url_len + 1); /* Format label, not yet subject to any size limitations */ size_t chars = key_len + (show_url ? (2 + url_len) : 0); char32_t label[chars + 1]; label[chars] = U'\0'; if (show_url) { c32cpy(label, key); c32cat(label, U": "); c32cat(label, url_wchars); } else c32ncpy(label, key, chars); /* Upper case the key characters */ for (size_t i = 0; i < c32len(key); i++) label[i] = toc32upper(label[i]); /* Blank already entered key characters */ for (size_t i = 0; i < entered_key_len; i++) label[i] = U' '; /* * Don't extend outside our window * * Truncate label so that it doesn't extend outside our * window. * * Do it in a way such that we don't cut the label in the * middle of a double-width character. */ int cols = 0; for (size_t i = 0; i <= c32len(label); i++) { int _cols = c32swidth(label, i); if (_cols == (size_t)-1) continue; if (_cols >= max_cols) { if (i > 0) label[i - 1] = U'…'; label[i] = U'\0'; cols = max_cols; break; } cols = _cols; } if (cols == 0) continue; int width = x_margin + cols * term->cell_width + x_margin; int height = y_margin + term->cell_height + y_margin; width = roundf(scale * ceilf(width / scale)); height = roundf(scale * ceilf(height / scale)); info[render_count].url = &it->item; info[render_count].text = xc32dup(label); info[render_count].x = x; info[render_count].y = y; widths[render_count] = width; heights[render_count] = height; render_count++; } struct buffer_chain *chain = term->render.chains.url; struct buffer *bufs[render_count]; shm_get_many(chain, render_count, widths, heights, bufs, false); uint32_t fg = term->conf->colors.use_custom.jump_label ? term->conf->colors.jump_label.fg : term->colors.table[0]; uint32_t bg = term->conf->colors.use_custom.jump_label ? term->conf->colors.jump_label.bg : term->colors.table[3]; for (size_t i = 0; i < render_count; i++) { const struct wayl_sub_surface *sub_surf = &info[i].url->surf; const char32_t *label = info[i].text; const int x = info[i].x; const int y = info[i].y; xassert(sub_surf->surface.surf != NULL); xassert(sub_surf->sub != NULL); wl_subsurface_set_position( sub_surf->sub, roundf((term->margins.left + x) / scale), roundf((term->margins.top + y) / scale)); render_osd( term, sub_surf, term->fonts[0], bufs[i], label, fg, 0xffu << 24 | bg, x_margin); free(info[i].text); } } static void render_update_title(struct terminal *term) { static const size_t max_len = 2048; const char *title = term->window_title != NULL ? term->window_title : "foot"; char *copy = NULL; if (strlen(title) > max_len) { copy = xstrndup(title, max_len); title = copy; } xdg_toplevel_set_title(term->window->xdg_toplevel, title); free(copy); } static void frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) { struct terminal *term = data; xassert(term->window->frame_callback == wl_callback); wl_callback_destroy(wl_callback); term->window->frame_callback = NULL; bool grid = term->render.pending.grid; bool csd = term->render.pending.csd; bool search = term->is_searching && term->render.pending.search; bool urls = urls_mode_is_active(term) > 0 && term->render.pending.urls; term->render.pending.grid = false; term->render.pending.csd = false; term->render.pending.search = false; term->render.pending.urls = false; struct grid *original_grid = term->grid; if (urls_mode_is_active(term)) { xassert(term->url_grid_snapshot != NULL); term->grid = term->url_grid_snapshot; } if (csd && term->window->csd_mode == CSD_YES) { quirk_weston_csd_on(term); render_csd(term); quirk_weston_csd_off(term); } if (search) render_search_box(term); if (urls) render_urls(term); if ((grid && !term->delayed_render_timer.is_armed) || (csd | search | urls)) grid_render(term); tll_foreach(term->wl->seats, it) { if (it->item.ime_focus == term) ime_update_cursor_rect(&it->item); } term->grid = original_grid; } static void tiocswinsz(struct terminal *term) { if (term->ptmx >= 0) { if (ioctl(term->ptmx, (unsigned int)TIOCSWINSZ, &(struct winsize){ .ws_row = term->rows, .ws_col = term->cols, .ws_xpixel = term->cols * term->cell_width, .ws_ypixel = term->rows * term->cell_height}) < 0) { LOG_ERRNO("TIOCSWINSZ"); } term_send_size_notification(term); } } static void delayed_reflow_of_normal_grid(struct terminal *term) { if (term->interactive_resizing.grid == NULL) return; xassert(term->interactive_resizing.new_rows > 0); struct coord *const tracking_points[] = { &term->selection.coords.start, &term->selection.coords.end, }; /* Reflow the original (since before the resize was started) grid, * to the *current* dimensions */ grid_resize_and_reflow( term->interactive_resizing.grid, term, term->interactive_resizing.new_rows, term->normal.num_cols, term->interactive_resizing.old_screen_rows, term->rows, term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, tracking_points); /* Replace the current, truncated, "normal" grid with the * correctly reflowed one */ grid_free(&term->normal); term->normal = *term->interactive_resizing.grid; free(term->interactive_resizing.grid); term->hide_cursor = term->interactive_resizing.old_hide_cursor; /* Reset */ term->interactive_resizing.grid = NULL; term->interactive_resizing.old_screen_rows = 0; term->interactive_resizing.new_rows = 0; term->interactive_resizing.old_hide_cursor = false; /* Invalidate render pointers */ shm_unref(term->render.last_buf); term->render.last_buf = NULL; term->render.last_cursor.row = NULL; tll_free(term->normal.scroll_damage); sixel_reflow_grid(term, &term->normal); if (term->grid == &term->normal) { term_damage_view(term); render_refresh(term); } term_ptmx_resume(term); } static bool fdm_tiocswinsz(struct fdm *fdm, int fd, int events, void *data) { struct terminal *term = data; if (events & EPOLLIN) { tiocswinsz(term); delayed_reflow_of_normal_grid(term); } if (term->window->resize_timeout_fd >= 0) { fdm_del(fdm, term->window->resize_timeout_fd); term->window->resize_timeout_fd = -1; } return true; } static void send_dimensions_to_client(struct terminal *term) { struct wl_window *win = term->window; if (!win->is_resizing || term->conf->resize_delay_ms == 0) { /* Send new dimensions to client immediately */ tiocswinsz(term); delayed_reflow_of_normal_grid(term); /* And make sure to reset and deallocate a lingering timer */ if (win->resize_timeout_fd >= 0) { fdm_del(term->fdm, win->resize_timeout_fd); win->resize_timeout_fd = -1; } } else { /* Send new dimensions to client "in a while" */ assert(win->is_resizing && term->conf->resize_delay_ms > 0); int fd = win->resize_timeout_fd; uint16_t delay_ms = term->conf->resize_delay_ms; bool successfully_scheduled = false; if (fd < 0) { /* Lazy create timer fd */ fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (fd < 0) LOG_ERRNO("failed to create TIOCSWINSZ timer"); else if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_tiocswinsz, term)) { close(fd); fd = -1; } win->resize_timeout_fd = fd; } if (fd >= 0) { /* Reset timeout */ const struct itimerspec timeout = { .it_value = { .tv_sec = delay_ms / 1000, .tv_nsec = (delay_ms % 1000) * 1000000, }, }; if (timerfd_settime(fd, 0, &timeout, NULL) < 0) { LOG_ERRNO("failed to arm TIOCSWINSZ timer"); fdm_del(term->fdm, fd); win->resize_timeout_fd = -1; } else successfully_scheduled = true; } if (!successfully_scheduled) { tiocswinsz(term); delayed_reflow_of_normal_grid(term); } } } static void set_size_from_grid(struct terminal *term, int *width, int *height, int cols, int rows) { int new_width, new_height; /* Nominal grid dimensions */ new_width = cols * term->cell_width; new_height = rows * term->cell_height; /* Include any configured padding */ new_width += 2 * term->conf->pad_x * term->scale; new_height += 2 * term->conf->pad_y * term->scale; /* Round to multiples of scale */ new_width = round(term->scale * round(new_width / term->scale)); new_height = round(term->scale * round(new_height / term->scale)); if (width != NULL) *width = new_width; if (height != NULL) *height = new_height; } /* Move to terminal.c? */ bool render_resize(struct terminal *term, int width, int height, uint8_t opts) { if (term->shutdown.in_progress) return false; if (!term->window->is_configured) return false; if (term->cell_width == 0 && term->cell_height == 0) return false; const bool is_floating = !term->window->is_maximized && !term->window->is_fullscreen && !term->window->is_tiled; /* Convert logical size to physical size */ const float scale = term->scale; width = round(width * scale); height = round(height * scale); /* If the grid should be kept, the size should be overridden */ if (is_floating && (opts & RESIZE_KEEP_GRID)) { set_size_from_grid(term, &width, &height, term->cols, term->rows); } if (width == 0) { /* The compositor is letting us choose the width */ if (term->stashed_width != 0) { /* If a default size is requested, prefer the "last used" size */ width = term->stashed_width; } else { /* Otherwise, use a user-configured size */ switch (term->conf->size.type) { case CONF_SIZE_PX: width = term->conf->size.width; if (wayl_win_csd_borders_visible(term->window)) width -= 2 * term->conf->csd.border_width_visible; width *= scale; break; case CONF_SIZE_CELLS: set_size_from_grid(term, &width, NULL, term->conf->size.width, term->conf->size.height); break; } } } if (height == 0) { /* The compositor is letting us choose the height */ if (term->stashed_height != 0) { /* If a default size is requested, prefer the "last used" size */ height = term->stashed_height; } else { /* Otherwise, use a user-configured size */ switch (term->conf->size.type) { case CONF_SIZE_PX: height = term->conf->size.height; /* Take CSDs into account */ if (wayl_win_csd_titlebar_visible(term->window)) height -= term->conf->csd.title_height; if (wayl_win_csd_borders_visible(term->window)) height -= 2 * term->conf->csd.border_width_visible; height *= scale; break; case CONF_SIZE_CELLS: set_size_from_grid(term, NULL, &height, term->conf->size.width, term->conf->size.height); break; } } } /* Don't shrink grid too much */ const int min_cols = 1; const int min_rows = 1; /* Minimum window size (must be divisible by the scaling factor)*/ const int min_width = roundf(scale * ceilf((min_cols * term->cell_width) / scale)); const int min_height = roundf(scale * ceilf((min_rows * term->cell_height) / scale)); width = max(width, min_width); height = max(height, min_height); /* Padding */ const int max_pad_x = (width - min_width) / 2; const int max_pad_y = (height - min_height) / 2; 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); if (is_floating && (opts & RESIZE_BY_CELLS) && term->conf->resize_by_cells) { /* If resizing in cell increments, restrict the width and height */ width = ((width - 2 * pad_x) / term->cell_width) * term->cell_width + 2 * pad_x; width = max(min_width, roundf(scale * roundf(width / scale))); height = ((height - 2 * pad_y) / term->cell_height) * term->cell_height + 2 * pad_y; height = max(min_height, roundf(scale * roundf(height / scale))); } if (!(opts & RESIZE_FORCE) && width == term->width && height == term->height && scale == term->scale) { return false; } /* Cancel an application initiated "Synchronized Update" */ term_disable_app_sync_updates(term); /* Drop out of URL mode */ urls_reset(term); LOG_DBG("resized: size=%dx%d (scale=%.2f)", width, height, term->scale); term->width = width; term->height = height; /* Screen rows/cols before resize */ int old_cols = term->cols; int old_rows = term->rows; /* Screen rows/cols after resize */ const int new_cols = (term->width - 2 * pad_x) / term->cell_width; const int new_rows = (term->height - 2 * pad_y) / term->cell_height; /* * Requirements for scrollback: * * a) total number of rows (visible + scrollback history) must be * a power of two * b) must be representable in a plain int (signed) * * This means that on a "normal" system, where ints are 32-bit, * the largest possible scrollback size is 1073741824 (0x40000000, * 1 << 30). * * The largest *signed* int is 2147483647 (0x7fffffff), which is * *not* a power of two. * * Note that these are theoretical limits. Most of the time, * you'll get a memory allocation failure when trying to allocate * the grid array. */ const unsigned max_scrollback = (INT_MAX >> 1) + 1; const unsigned scrollback_lines_not_yet_power_of_two = min((uint64_t)term->render.scrollback_lines + new_rows - 1, max_scrollback); /* Grid rows/cols after resize */ const int new_normal_grid_rows = min(1u << (32 - __builtin_clz(scrollback_lines_not_yet_power_of_two)), max_scrollback); const int new_alt_grid_rows = min(1u << (32 - __builtin_clz(new_rows)), max_scrollback); LOG_DBG("grid rows: %d", new_normal_grid_rows); xassert(new_cols >= 1); xassert(new_rows >= 1); /* Margins */ const int grid_width = new_cols * term->cell_width; const int grid_height = new_rows * term->cell_height; const int total_x_pad = term->width - grid_width; const int total_y_pad = term->height - grid_height; const bool centered_padding = term->conf->center || term->window->is_fullscreen || term->window->is_maximized; 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.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); if (new_cols == old_cols && new_rows == old_rows) { LOG_DBG("grid layout unaffected; skipping reflow"); term->interactive_resizing.new_rows = new_normal_grid_rows; goto damage_view; } /* * Since text reflow is slow, don't do it *while* resizing. Only * do it when done, or after "pausing" the resize for sufficiently * long. We reuse the TIOCSWINSZ timer to handle this. See * send_dimensions_to_client() and fdm_tiocswinsz(). * * To be able to do the final reflow correctly, we need a copy of * the original grid, before the resize started. */ if (term->window->is_resizing && term->conf->resize_delay_ms > 0) { if (term->interactive_resizing.grid == NULL) { term_ptmx_pause(term); /* Stash the current 'normal' grid, as-is, to be used when * doing the final reflow */ term->interactive_resizing.old_screen_rows = term->rows; term->interactive_resizing.old_cols = term->cols; term->interactive_resizing.old_hide_cursor = term->hide_cursor; term->interactive_resizing.grid = xmalloc(sizeof(*term->interactive_resizing.grid)); *term->interactive_resizing.grid = term->normal; if (term->grid == &term->normal) term->interactive_resizing.selection_coords = term->selection.coords; } else { /* We'll replace the current temporary grid, with a new * one (again based on the original grid) */ grid_free(&term->normal); } struct grid *orig = term->interactive_resizing.grid; /* * Copy the current viewport (of the original grid) to a new * grid that will be used during the resize. For now, throw * away sixels and OSC-8 URLs. They'll be "restored" when we * do the final reflow. * * Note that OSC-8 URLs are perfectly ok to throw away; they * cannot be interacted with during the resize. And, even if * url.osc8-underline=always, the "underline" attribute is * part of the cell, not the URI struct (and thus our faked * grid will still render OSC-8 links underlined). * * TODO: * - sixels? */ struct grid g = { .num_rows = 1 << (32 - __builtin_clz(term->interactive_resizing.old_screen_rows)), .num_cols = term->interactive_resizing.old_cols, .offset = 0, .view = 0, .cursor = orig->cursor, .saved_cursor = orig->saved_cursor, .rows = xcalloc(g.num_rows, sizeof(g.rows[0])), .cur_row = NULL, .scroll_damage = tll_init(), .sixel_images = tll_init(), .kitty_kbd = orig->kitty_kbd, }; term->selection.coords.start.row -= orig->view; term->selection.coords.end.row -= orig->view; for (size_t i = 0, j = orig->view; i < term->interactive_resizing.old_screen_rows; i++, j = (j + 1) & (orig->num_rows - 1)) { g.rows[i] = grid_row_alloc(g.num_cols, false); memcpy(g.rows[i]->cells, orig->rows[j]->cells, g.num_cols * sizeof(g.rows[i]->cells[0])); if (orig->rows[j]->extra == NULL || orig->rows[j]->extra->underline_ranges.count == 0) { continue; } /* * Copy underline ranges */ const struct row_ranges *underline_src = &orig->rows[j]->extra->underline_ranges; const int count = underline_src->count; g.rows[i]->extra = xcalloc(1, sizeof(*g.rows[i]->extra)); g.rows[i]->extra->underline_ranges.v = xmalloc( count * sizeof(g.rows[i]->extra->underline_ranges.v[0])); struct row_ranges *underline_dst = &g.rows[i]->extra->underline_ranges; underline_dst->count = underline_dst->size = count; for (int k = 0; k < count; k++) underline_dst->v[k] = underline_src->v[k]; } term->normal = g; term->hide_cursor = true; } if (term->grid == &term->alt) selection_cancel(term); else { /* * Don't cancel, but make sure there aren't any ongoing * selections after the resize. */ tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) selection_finalize(&it->item, term, it->item.pointer.serial); } } /* * TODO: if we remove the selection_finalize() call above (i.e. if * we start allowing selections to be ongoing across resizes), the * selection's pivot point coordinates *must* be added to the * tracking points list. */ /* Resize grids */ if (term->window->is_resizing && term->conf->resize_delay_ms > 0) { /* Simple truncating resize, *while* an interactive resize is * ongoing. */ xassert(term->interactive_resizing.grid != NULL); xassert(new_normal_grid_rows > 0); term->interactive_resizing.new_rows = new_normal_grid_rows; grid_resize_without_reflow( &term->normal, new_alt_grid_rows, new_cols, term->interactive_resizing.old_screen_rows, new_rows); } else { /* Full text reflow */ int old_normal_rows = old_rows; if (term->interactive_resizing.grid != NULL) { /* Throw away the current, truncated, "normal" grid, and * use the original grid instead (from before the resize * started) */ grid_free(&term->normal); term->normal = *term->interactive_resizing.grid; free(term->interactive_resizing.grid); term->hide_cursor = term->interactive_resizing.old_hide_cursor; term->selection.coords = term->interactive_resizing.selection_coords; old_normal_rows = term->interactive_resizing.old_screen_rows; term->interactive_resizing.grid = NULL; term->interactive_resizing.old_screen_rows = 0; term->interactive_resizing.new_rows = 0; term->interactive_resizing.old_hide_cursor = false; term->interactive_resizing.selection_coords = (struct range){{-1, -1}, {-1, -1}}; term_ptmx_resume(term); } struct coord *const tracking_points[] = { &term->selection.coords.start, &term->selection.coords.end, }; grid_resize_and_reflow( &term->normal, term, new_normal_grid_rows, new_cols, old_normal_rows, new_rows, term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, tracking_points); } grid_resize_without_reflow( &term->alt, new_alt_grid_rows, new_cols, old_rows, new_rows); /* Reset tab stops */ tll_free(term->tab_stops); for (int c = 0; c < new_cols; c += 8) tll_push_back(term->tab_stops, c); term->cols = new_cols; term->rows = new_rows; sixel_reflow(term); LOG_DBG("resized: grid: cols=%d, rows=%d " "(left-margin=%d, right-margin=%d, top-margin=%d, bottom-margin=%d)", term->cols, term->rows, term->margins.left, term->margins.right, term->margins.top, term->margins.bottom); if (term->scroll_region.start >= term->rows) term->scroll_region.start = 0; if (term->scroll_region.end > term->rows || term->scroll_region.end >= old_rows) { term->scroll_region.end = term->rows; } term->render.last_cursor.row = NULL; damage_view: /* Signal TIOCSWINSZ */ send_dimensions_to_client(term); if (is_floating) { /* Stash current size, to enable us to restore it when we're * being un-maximized/fullscreened/tiled */ term->stashed_width = term->width; term->stashed_height = term->height; } { const bool title_shown = wayl_win_csd_titlebar_visible(term->window); const bool border_shown = wayl_win_csd_borders_visible(term->window); const int title = title_shown ? roundf(term->conf->csd.title_height * scale) : 0; const int border = border_shown ? roundf(term->conf->csd.border_width_visible * scale) : 0; /* Must use surface logical coordinates (same calculations as in get_csd_data(), but with different inputs) */ const int toplevel_min_width = roundf(border / scale) + roundf(min_width / scale) + roundf(border / scale); const int toplevel_min_height = roundf(border / scale) + roundf(title / scale) + roundf(min_height / scale) + roundf(border / scale); const int toplevel_width = roundf(border / scale) + roundf(term->width / scale) + roundf(border / scale); const int toplevel_height = roundf(border / scale) + roundf(title / scale) + roundf(term->height / scale) + roundf(border / scale); const int x = roundf(-border / scale); const int y = roundf(-title / scale) - roundf(border / scale); xdg_toplevel_set_min_size( term->window->xdg_toplevel, toplevel_min_width, toplevel_min_height); xdg_surface_set_window_geometry( term->window->xdg_surface, x, y, toplevel_width, toplevel_height); } tll_free(term->normal.scroll_damage); tll_free(term->alt.scroll_damage); shm_unref(term->render.last_buf); term->render.last_buf = NULL; term_damage_view(term); render_refresh_csd(term); render_refresh_search(term); render_refresh(term); return true; } static void xcursor_callback( void *data, struct wl_callback *wl_callback, uint32_t callback_data); static const struct wl_callback_listener xcursor_listener = { .done = &xcursor_callback, }; bool render_xcursor_is_valid(const struct seat *seat, const char *cursor) { if (cursor == NULL) return false; if (seat->pointer.theme == NULL) return false; return wl_cursor_theme_get_cursor(seat->pointer.theme, cursor) != NULL; } static void render_xcursor_update(struct seat *seat) { /* If called from a frame callback, we may no longer have mouse focus */ if (!seat->mouse_focus) return; xassert(seat->pointer.shape != CURSOR_SHAPE_NONE); if (seat->pointer.shape == CURSOR_SHAPE_HIDDEN) { /* Hide cursor */ LOG_DBG("hiding cursor using client-side NULL-surface"); wl_surface_attach(seat->pointer.surface.surf, NULL, 0, 0); wl_pointer_set_cursor( seat->wl_pointer, seat->pointer.serial, seat->pointer.surface.surf, 0, 0); wl_surface_commit(seat->pointer.surface.surf); return; } const enum cursor_shape shape = seat->pointer.shape; const char *const xcursor = seat->pointer.last_custom_xcursor; if (seat->pointer.shape_device != NULL) { xassert(shape != CURSOR_SHAPE_CUSTOM || xcursor != NULL); const enum wp_cursor_shape_device_v1_shape custom_shape = (shape == CURSOR_SHAPE_CUSTOM && xcursor != NULL ? cursor_string_to_server_shape(xcursor) : 0); if (shape != CURSOR_SHAPE_CUSTOM || custom_shape != 0) { xassert(custom_shape == 0 || shape == CURSOR_SHAPE_CUSTOM); const enum wp_cursor_shape_device_v1_shape wp_shape = custom_shape != 0 ? custom_shape : cursor_shape_to_server_shape(shape); LOG_DBG("setting %scursor shape using cursor-shape-v1", custom_shape != 0 ? "custom " : ""); wp_cursor_shape_device_v1_set_shape( seat->pointer.shape_device, seat->pointer.serial, wp_shape); return; } } LOG_DBG("setting %scursor shape using a client-side cursor surface", seat->pointer.shape == CURSOR_SHAPE_CUSTOM ? "custom " : ""); if (seat->pointer.cursor == NULL) { /* * Normally, we never get here with a NULL-cursor, because we * only schedule a cursor update when we succeed to load the * cursor image. * * However, it is possible that we did succeed to load an * image, and scheduled an update. But, *before* the scheduled * update triggers, the user mvoes the pointer, and we try to * load a new cursor image. This time failing. * * In this case, we have a NULL cursor, but the scheduled * update is still scheduled. */ return; } const float scale = seat->pointer.scale; struct wl_cursor_image *image = seat->pointer.cursor->images[0]; struct wl_buffer *buf = wl_cursor_image_get_buffer(image); wayl_surface_scale_explicit_width_height( seat->mouse_focus->window, &seat->pointer.surface, image->width, image->height, scale); wl_surface_attach(seat->pointer.surface.surf, buf, 0, 0); wl_pointer_set_cursor( seat->wl_pointer, seat->pointer.serial, seat->pointer.surface.surf, image->hotspot_x / scale, image->hotspot_y / scale); wl_surface_damage_buffer( seat->pointer.surface.surf, 0, 0, INT32_MAX, INT32_MAX); xassert(seat->pointer.xcursor_callback == NULL); seat->pointer.xcursor_callback = wl_surface_frame(seat->pointer.surface.surf); wl_callback_add_listener(seat->pointer.xcursor_callback, &xcursor_listener, seat); wl_surface_commit(seat->pointer.surface.surf); } static void xcursor_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) { struct seat *seat = data; xassert(seat->pointer.xcursor_callback == wl_callback); wl_callback_destroy(wl_callback); seat->pointer.xcursor_callback = NULL; if (seat->pointer.xcursor_pending) { render_xcursor_update(seat); seat->pointer.xcursor_pending = false; } } static void fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) { struct renderer *renderer = data; struct wayland *wayl = renderer->wayl; tll_foreach(renderer->wayl->terms, it) { struct terminal *term = it->item; if (unlikely(term->shutdown.in_progress || !term->window->is_configured)) continue; bool grid = term->render.refresh.grid; bool csd = term->render.refresh.csd; bool search = term->is_searching && term->render.refresh.search; bool urls = urls_mode_is_active(term) && term->render.refresh.urls; if (!(grid | csd | search | urls)) continue; if (term->render.app_sync_updates.enabled && !(csd | search | urls)) continue; term->render.refresh.grid = false; term->render.refresh.csd = false; term->render.refresh.search = false; term->render.refresh.urls = false; if (term->window->frame_callback == NULL) { struct grid *original_grid = term->grid; if (urls_mode_is_active(term)) { xassert(term->url_grid_snapshot != NULL); term->grid = term->url_grid_snapshot; } if (csd && term->window->csd_mode == CSD_YES) { quirk_weston_csd_on(term); render_csd(term); quirk_weston_csd_off(term); } if (search) render_search_box(term); if (urls) render_urls(term); if (grid | csd | search | urls) grid_render(term); tll_foreach(term->wl->seats, it) { if (it->item.ime_focus == term) ime_update_cursor_rect(&it->item); } term->grid = original_grid; } else { /* Tells the frame callback to render again */ term->render.pending.grid |= grid; term->render.pending.csd |= csd; term->render.pending.search |= search; term->render.pending.urls |= urls; } } tll_foreach(wayl->seats, it) { if (it->item.pointer.xcursor_pending) { if (it->item.pointer.xcursor_callback == NULL) { render_xcursor_update(&it->item); it->item.pointer.xcursor_pending = false; } else { /* Frame callback will call render_xcursor_update() */ } } } } void render_refresh_title(struct terminal *term) { struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) return; struct timespec diff; timespec_sub(&now, &term->render.title.last_update, &diff); if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { const struct itimerspec timeout = { .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, }; timerfd_settime(term->render.title.timer_fd, 0, &timeout, NULL); } else { term->render.title.last_update = now; render_update_title(term); } render_refresh_csd(term); } void render_refresh_app_id(struct terminal *term) { struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) return; struct timespec diff; timespec_sub(&now, &term->render.app_id.last_update, &diff); if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { const struct itimerspec timeout = { .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, }; timerfd_settime(term->render.app_id.timer_fd, 0, &timeout, NULL); return; } const char *app_id = term->app_id != NULL ? term->app_id : term->conf->app_id; xdg_toplevel_set_app_id(term->window->xdg_toplevel, app_id); term->render.app_id.last_update = now; } void render_refresh_icon(struct terminal *term) { if (term->wl->toplevel_icon_manager == NULL) { LOG_DBG("compositor does not implement xdg-toplevel-icon: " "ignoring request to refresh window icon"); return; } struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) return; struct timespec diff; timespec_sub(&now, &term->render.icon.last_update, &diff); if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { const struct itimerspec timeout = { .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, }; timerfd_settime(term->render.icon.timer_fd, 0, &timeout, NULL); return; } const char *icon_name = term_icon(term); LOG_DBG("setting toplevel icon: %s", icon_name); struct xdg_toplevel_icon_v1 *icon = xdg_toplevel_icon_manager_v1_create_icon(term->wl->toplevel_icon_manager); xdg_toplevel_icon_v1_set_name(icon, icon_name); xdg_toplevel_icon_manager_v1_set_icon( term->wl->toplevel_icon_manager, term->window->xdg_toplevel, icon); xdg_toplevel_icon_v1_destroy(icon); term->render.icon.last_update = now; } void render_refresh(struct terminal *term) { term->render.refresh.grid = true; } void render_refresh_csd(struct terminal *term) { if (term->window->csd_mode == CSD_YES) term->render.refresh.csd = true; } void render_refresh_search(struct terminal *term) { if (term->is_searching) term->render.refresh.search = true; } void render_refresh_urls(struct terminal *term) { if (urls_mode_is_active(term)) term->render.refresh.urls = true; } bool render_xcursor_set(struct seat *seat, struct terminal *term, enum cursor_shape shape) { if (seat->pointer.theme == NULL && seat->pointer.shape_device == NULL) return false; if (seat->mouse_focus == NULL) { seat->pointer.shape = CURSOR_SHAPE_NONE; return true; } if (seat->mouse_focus != term) { /* This terminal doesn't have mouse focus */ return true; } if (seat->pointer.shape == shape && !(shape == CURSOR_SHAPE_CUSTOM && !streq(seat->pointer.last_custom_xcursor, term->mouse_user_cursor))) { return true; } if (shape == CURSOR_SHAPE_HIDDEN) { seat->pointer.cursor = NULL; free(seat->pointer.last_custom_xcursor); seat->pointer.last_custom_xcursor = NULL; } else if (seat->pointer.shape_device == NULL) { const char *const custom_xcursors[] = {term->mouse_user_cursor, NULL}; const char *const *xcursors = shape == CURSOR_SHAPE_CUSTOM ? custom_xcursors : cursor_shape_to_string(shape); xassert(xcursors[0] != NULL); seat->pointer.cursor = NULL; for (size_t i = 0; xcursors[i] != NULL; i++) { seat->pointer.cursor = wl_cursor_theme_get_cursor(seat->pointer.theme, xcursors[i]); if (seat->pointer.cursor != NULL) { LOG_DBG("loaded xcursor %s", xcursors[i]); break; } } if (seat->pointer.cursor == NULL) { LOG_ERR( "failed to load xcursor pointer '%s', and all of its fallbacks", xcursors[0]); return false; } } else { /* Server-side cursors - no need to load anything */ } if (shape == CURSOR_SHAPE_CUSTOM) { free(seat->pointer.last_custom_xcursor); seat->pointer.last_custom_xcursor = xstrdup(term->mouse_user_cursor); } /* FDM hook takes care of actual rendering */ seat->pointer.shape = shape; seat->pointer.xcursor_pending = true; return true; } bool render_do_linear_blending(const struct terminal *term) { return term->conf->gamma_correct != GAMMA_CORRECT_DISABLED && term->wl->color_management.img_description != NULL; } foot-1.21.0/render.h000066400000000000000000000025441476600145200142040ustar00rootroot00000000000000#pragma once #include #include "terminal.h" #include "fdm.h" #include "wayland.h" #include "misc.h" struct renderer; struct renderer *render_init(struct fdm *fdm, struct wayland *wayl); void render_destroy(struct renderer *renderer); enum resize_options { RESIZE_NORMAL = 0, RESIZE_FORCE = 1 << 0, RESIZE_BY_CELLS = 1 << 1, RESIZE_KEEP_GRID = 1 << 2, }; bool render_resize( struct terminal *term, int width, int height, uint8_t resize_options); void render_refresh(struct terminal *term); void render_refresh_app_id(struct terminal *term); void render_refresh_icon(struct terminal *term); void render_refresh_csd(struct terminal *term); void render_refresh_search(struct terminal *term); void render_refresh_title(struct terminal *term); void render_refresh_urls(struct terminal *term); bool render_xcursor_set( struct seat *seat, struct terminal *term, enum cursor_shape shape); bool render_xcursor_is_valid(const struct seat *seat, const char *cursor); void render_overlay(struct terminal *term); struct render_worker_context { int my_id; struct terminal *term; }; int render_worker_thread(void *_ctx); struct csd_data { int x; int y; int width; int height; }; struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx); bool render_do_linear_blending(const struct terminal *term); foot-1.21.0/scripts/000077500000000000000000000000001476600145200142365ustar00rootroot00000000000000foot-1.21.0/scripts/benchmark.py000077500000000000000000000024261476600145200165510ustar00rootroot00000000000000#!/usr/bin/env -S python3 -u import argparse import fcntl import os import statistics import struct import sys import termios from datetime import datetime def main(): parser = argparse.ArgumentParser() parser.add_argument('files', type=argparse.FileType('rb'), nargs='+') parser.add_argument('--iterations', type=int, default=20) args = parser.parse_args() lines, cols, height, width = struct.unpack( 'HHHH', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))) times = {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): start = datetime.now() sys.stdout.buffer.write(bench_bytes) stop = datetime.now() times[f.name].append((stop - start).total_seconds()) del bench_bytes print('\033[J') print(times) print(f'cols={cols}, lines={lines}, width={width}px, height={height}px') for f in args.files: print(f'{os.path.basename(f.name)}: ' f'{statistics.mean(times[f.name]):.3f}s ' f'±{statistics.stdev(times[f.name]):.3f}') if __name__ == '__main__': sys.exit(main()) foot-1.21.0/scripts/generate-alt-random-writes.py000077500000000000000000000223101476600145200217520ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import enum import fcntl import random import signal import struct import sys import termios class ColorVariant(enum.IntEnum): NONE = enum.auto() REGULAR = enum.auto() BRIGHT = enum.auto() CUBE = enum.auto() RGB = enum.auto() def main(): parser = argparse.ArgumentParser() parser.add_argument( 'out', type=argparse.FileType(mode='w'), nargs='?', help='name of output file') parser.add_argument('--cols', type=int) parser.add_argument('--rows', type=int) parser.add_argument('--colors-regular', action='store_true') parser.add_argument('--colors-bright', action='store_true') parser.add_argument('--colors-256', action='store_true') parser.add_argument('--colors-rgb', action='store_true') parser.add_argument('--scroll', action='store_true') parser.add_argument('--scroll-region', action='store_true') parser.add_argument('--attr-bold', action='store_true') parser.add_argument('--attr-italic', action='store_true') parser.add_argument('--attr-underline', action='store_true') parser.add_argument('--sixel', action='store_true') parser.add_argument('--seed', type=int) opts = parser.parse_args() out = opts.out if opts.out is not None else sys.stdout if opts.rows is None or opts.cols is None: try: def dummy(*args): """Need a handler installed for sigwait() to trigger.""" pass signal.signal(signal.SIGWINCH, dummy) while True: with open('/dev/tty', 'rb') as pty: lines, cols, height, width = struct.unpack( 'HHHH', fcntl.ioctl(pty, termios.TIOCGWINSZ, struct.pack('HHHH', 0, 0, 0, 0))) if width > 0 and height > 0: break # We’re early; the foot window hasn’t been mapped yet. Or, # to be more precise, fonts haven’t yet been loaded, # meaning it doesn’t have any cell geometry yet. signal.sigwait([signal.SIGWINCH]) signal.signal(signal.SIGWINCH, signal.SIG_DFL) except OSError: lines = None cols = None height = None width = None if opts.rows is not None: lines = opts.rows height = 15 * lines # PGO helper binary hardcodes cell height to 15px if opts.cols is not None: cols = opts.cols width = 8 * cols # PGO help binary hardcodes cell width to 8px if lines is None or cols is None or height is None or width is None: raise Exception('could not get terminal width/height; use --rows and --cols') assert lines > 0, f'{lines}' assert cols > 0, f'{cols}' assert width > 0, f'{width}' assert height > 0, f'{height}' # Number of characters to write to screen count = 256 * 1024**1 # Characters to choose from alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRTSTUVWXYZ0123456789 öäå 👨👩🧒👩🏽‍🔬🇸🇪' color_variants = ([ColorVariant.NONE] + ([ColorVariant.REGULAR] if opts.colors_regular else []) + ([ColorVariant.BRIGHT] if opts.colors_bright else []) + ([ColorVariant.CUBE] if opts.colors_256 else []) + ([ColorVariant.RGB] if opts.colors_rgb else [])) # Enter alt screen out.write('\033[?1049h') # uses system time or /dev/urandom if available if opt.seed == None # pin seeding method to make seeding stable across future versions random.seed(a=opts.seed, version=2) for _ in range(count): if opts.scroll and random.randrange(256) == 0: out.write('\033[m') if opts.scroll_region and random.randrange(256) == 0: top = random.randrange(3) bottom = random.randrange(3) out.write(f'\033[{top};{lines - bottom}r') lines_to_scroll = random.randrange(lines - 1) rev = random.randrange(2) if not rev and random.randrange(2): out.write(f'\033[{lines};{cols}H') out.write('\n' * lines_to_scroll) else: out.write(f'\033[{lines_to_scroll + 1}{"T" if rev == 1 else "S"}') continue # Generate a random location and a random character row = random.randrange(lines) col = random.randrange(cols) c = random.choice(alphabet) repeat = random.randrange((cols - col) + 1) assert col + repeat <= cols color_variant = random.choice(color_variants) # Position cursor out.write(f'\033[{row + 1};{col + 1}H') if color_variant in [ColorVariant.REGULAR, ColorVariant.BRIGHT]: do_bg = random.randrange(2) base = 40 if do_bg else 30 base += 60 if color_variant == ColorVariant.BRIGHT else 0 idx = random.randrange(8) out.write(f'\033[{base + idx}m') elif color_variant == ColorVariant.CUBE: do_bg = random.randrange(2) base = 48 if do_bg else 38 idx = random.randrange(256) if random.randrange(2): # Old-style out.write(f'\033[{base};5;{idx}m') else: # New-style (sub-parameter based) out.write(f'\033[{base}:5:{idx}m') elif color_variant == ColorVariant.RGB: do_bg = random.randrange(2) base = 48 if do_bg else 38 # use list comprehension in favor of randbytes(n) # which is only available for Python >= 3.9 rgb = [random.randrange(256) for _ in range(3)] if random.randrange(2): # Old-style out.write(f'\033[{base};2;{rgb[0]};{rgb[1]};{rgb[2]}m') else: # New-style (sub-parameter based) out.write(f'\033[{base}:2::{rgb[0]}:{rgb[1]}:{rgb[2]}m') if opts.attr_bold and random.randrange(5) == 0: out.write('\033[1m') if opts.attr_italic and random.randrange(5) == 0: out.write('\033[3m') if opts.attr_underline and random.randrange(5) == 0: out.write('\033[4m') out.write(c * repeat) do_sgr_reset = random.randrange(2) if do_sgr_reset: reset_actions = ['\033[m', '\033[39m', '\033[49m'] out.write(random.choice(reset_actions)) # Reset colors out.write('\033[m\033[r') if opts.sixel: # The sixel 'alphabet' sixels = '?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~' last_pos = None last_size = None for _ in range(20): if last_pos is not None and random.randrange(2): # Overwrite last sixel. I.e. use same position and # size as last sixel pass else: # Random origin in upper left quadrant last_pos = random.randrange(lines // 2) + 1, random.randrange(cols // 2) + 1 last_size = random.randrange((height + 1) // 2), random.randrange((width + 1) // 2) out.write(f'\033[{last_pos[0]};{last_pos[1]}H') six_height, six_width = last_size six_rows = (six_height + 5) // 6 # Round up; each sixel is 6 pixels # Begin sixel (with P2 set to either 0 or 1 - opaque or transparent) sixel_p2 = random.randrange(2) out.write(f'\033P;{sixel_p2}q') # Sixel size. Without this, sixels will be # auto-resized on cell-boundaries. out.write(f'"1;1;{six_width};{six_height}') # Set up 256 random colors for idx in range(256): # param 2: 1=HLS, 2=RGB. # param 3/4/5: HLS/RGB values in range 0-100 # (except 'hue' which is 0..360) out.write(f'#{idx};2;{random.randrange(101)};{random.randrange(101)};{random.randrange(101)}') for row in range(six_rows): band_count = random.randrange(4, 33) for band in range(band_count): # Choose a random color out.write(f'#{random.randrange(256)}') if random.randrange(2): for col in range(six_width): out.write(f'{random.choice(sixels)}') else: pix_left = six_width while pix_left > 0: repeat_count = random.randrange(1, pix_left + 1) out.write(f'!{repeat_count}{random.choice(sixels)}') pix_left -= repeat_count # Next line if band + 1 < band_count: # Move cursor to beginning of current row out.write('$') elif row + 1 < six_rows: # Newline out.write('-') # End sixel out.write('\033\\') # Leave alt screen out.write('\033[?1049l') if __name__ == '__main__': sys.exit(main()) foot-1.21.0/scripts/generate-builtin-terminfo.py000077500000000000000000000133571476600145200217030ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import re import sys from typing import Dict, Union class Capability: def __init__(self, name: str, value: Union[bool, int, str]): self._name = name self._value = value @property def name(self) -> str: return self._name @property def value(self) -> Union[bool, int, str]: return self._value def __lt__(self, other): return self._name < other._name def __le__(self, other): return self._name <= other._name def __eq__(self, other): return self._name == other._name def __ne__(self, other): return self._name != other._name def __gt__(self, other): return self._name > other._name def __ge__(self, other): return self._name >= other._name class BoolCapability(Capability): def __init__(self, name: str): super().__init__(name, True) class IntCapability(Capability): pass class StringCapability(Capability): def __init__(self, name: str, value: str): # see terminfo(5) for valid escape sequences # Control characters def translate_ctrl_chr(m): ctrl = m.group(1) if ctrl == '?': return '\\x7f' return f'\\x{ord(ctrl) - ord("@"):02x}' value = re.sub(r'\^([@A-Z[\\\\\]^_?])', translate_ctrl_chr, value) # Ensure e.g. \E7 (or \e7) doesn’t get translated to “\0337”, # which would be interpreted as octal 337 by the C compiler value = re.sub(r'(\\E|\\e)([0-7])', r'\\033" "\2', value) # Replace \E and \e with ESC value = re.sub(r'\\E|\\e', r'\\033', value) # Unescape ,:^ value = re.sub(r'\\(,|:|\^)', r'\1', value) # Replace \s with space value = value.replace('\\s', ' ') # Let \\, \n, \r, \t, \b and \f "fall through", to the C string literal if re.search(r'\\l', value): raise NotImplementedError('\\l escape sequence') super().__init__(name, value) class Fragment: def __init__(self, name: str, description: str): self._name = name self._description = description self._caps = {} @property def name(self) -> str: return self._name @property def description(self) -> str: return self._description @property def caps(self) -> Dict[str, Capability]: return self._caps def add_capability(self, cap: Capability): assert cap.name not in self._caps self._caps[cap.name] = cap def del_capability(self, name: str): del self._caps[name] def main(): parser = argparse.ArgumentParser() parser.add_argument('source_entry_name') parser.add_argument('source', type=argparse.FileType('r')) parser.add_argument('target_entry_name') parser.add_argument('target', type=argparse.FileType('w')) opts = parser.parse_args() source_entry_name = opts.source_entry_name target_entry_name = opts.target_entry_name source = opts.source target = opts.target lines = [] for l in source.readlines(): l = l.strip() if l.startswith('#'): continue lines.append(l) fragments = {} cur_fragment = None for m in re.finditer( r'(?P(?P[-+\w@]+)\|(?P.+?),)|' r'(?P(?P\w+),)|' r'(?P(?P\w+)#(?P(0x)?[0-9a-fA-F]+),)|' r'(?P(?P\w+)=(?P(.+?)),)', ''.join(lines)): if m.group('name') is not None: name = m.group('entry_name') description = m.group('entry_desc') assert name not in fragments fragments[name] = Fragment(name, description) cur_fragment = fragments[name] elif m.group('bool_cap') is not None: name = m.group('bool_name') 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)) 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)) else: assert False # Expand ‘use’ capabilities for frag in fragments.values(): for cap in frag.caps.values(): if cap.name == 'use': use_frag = fragments[cap.value] for use_cap in use_frag.caps.values(): frag.add_capability(use_cap) frag.del_capability(cap.name) break entry = fragments[source_entry_name] try: entry.del_capability('RGB') except KeyError: pass entry.add_capability(IntCapability('Co', 256)) entry.add_capability(StringCapability('TN', target_entry_name)) entry.add_capability(StringCapability('name', target_entry_name)) entry.add_capability(IntCapability('RGB', 8)) # 8 bits per channel terminfo_parts = [] for cap in sorted(entry.caps.values()): name = cap.name value = str(cap.value) # Escape ‘“‘ name = name.replace('"', '\"') value = value.replace('"', '\"') terminfo_parts.append(name) if isinstance(cap, BoolCapability): terminfo_parts.append('') else: terminfo_parts.append(value) terminfo = '\\0" "'.join(terminfo_parts) target.write('#pragma once\n') target.write('\n') target.write(f'static const char terminfo_capabilities[] = "{terminfo}";') target.write('\n') if __name__ == '__main__': sys.exit(main()) foot-1.21.0/scripts/generate-emoji-variation-sequences.py000066400000000000000000000063751476600145200235010ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import sys 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(): parser = argparse.ArgumentParser() parser.add_argument('input', type=argparse.FileType('r')) parser.add_argument('output', type=argparse.FileType('w')) opts = parser.parse_args() codepoints: dict[int, Codepoint] = {} for line in opts.input: line = line.rstrip() if not line: continue if line[0] == '#': continue # Example: "0023 FE0E ; text style; # (1.1) NUMBER SIGN" cps, _ = line.split(';', maxsplit=1) # cps = "0023 FE0F " cps = cps.strip().split(' ') # cps = ["0023", "FE0F"] if len(cps) != 2: raise NotImplementedError(f'emoji variation sequences with more than one base codepoint: {cps}') cp, vs = cps # cp = "0023", vs = "FE0F" cp = int(cp, 16) # cp = 0x23 vs = int(vs, 16) # vs = 0xfe0f assert vs in [0xfe0e, 0xfe0f] if cp not in codepoints: codepoints[cp] = Codepoint(cp) assert codepoints[cp].start == cp if vs == 0xfe0e: codepoints[cp].vs15 = True else: codepoints[cp].vs16 = True sorted_list = sorted(codepoints.values(), key=lambda cp: cp.start) compacted: list[Codepoint] = [] for i, cp in enumerate(sorted_list): assert cp.end == cp.start if i == 0: compacted.append(cp) continue last_cp = compacted[-1] if last_cp.end == cp.start - 1 and last_cp.vs15 == cp.vs15 and last_cp.vs16 == cp.vs16: compacted[-1].end = cp.start else: compacted.append(cp) opts.output.write('#pragma once\n') opts.output.write('#include \n') opts.output.write('#include \n') opts.output.write('\n') opts.output.write('struct emoji_vs {\n') opts.output.write(' uint32_t start:21;\n') opts.output.write(' uint32_t end:21;\n') opts.output.write(' bool vs15:1;\n') opts.output.write(' bool vs16:1;\n') opts.output.write('} __attribute__((packed));\n') opts.output.write('_Static_assert(sizeof(struct emoji_vs) == 6, "unexpected struct size");\n') opts.output.write('\n') opts.output.write('#if defined(FOOT_GRAPHEME_CLUSTERING)\n') opts.output.write('\n') opts.output.write(f'static const struct emoji_vs emoji_vs[{len(compacted)}] = {{\n') for cp in compacted: opts.output.write(' {\n') opts.output.write(f' .start = 0x{cp.start:X},\n') opts.output.write(f' .end = 0x{cp.end:x},\n') opts.output.write(f' .vs15 = {"true" if cp.vs15 else "false"},\n') opts.output.write(f' .vs16 = {"true" if cp.vs16 else "false"},\n') opts.output.write(' },\n') opts.output.write('};\n') opts.output.write('\n') opts.output.write('#endif /* FOOT_GRAPHEME_CLUSTERING */\n') if __name__ == '__main__': sys.exit(main()) foot-1.21.0/scripts/srgb.py000077500000000000000000000050111476600145200155450ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import math import sys def srgb_to_linear(f: float) -> float: assert(f >= 0 and f <= 1.0) if f <= 0.04045: return f / 12.92 return math.pow((f + 0.055) / 1.055, 2.4) def linear_to_srgb(f: float) -> float: if f < 0.0031308: return f * 12.92 return 1.055 * math.pow(f, 1 / 2.4) - 0.055 def main(): 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] = [] srgb_table: list[int] = [] for i in range(256): linear_table.append(int(srgb_to_linear(float(i) / 255) * 65535 + 0.5)) for i in range(4096): srgb_table.append(int(linear_to_srgb(float(i) / 4095) * 255 + 0.5)) for i in range(256): while True: linear = linear_table[i] srgb = srgb_table[linear >> 4] if i == srgb: break linear_table[i] += 1 opts.h_output.write("#pragma once\n") opts.h_output.write("#include \n") opts.h_output.write("\n") opts.h_output.write('/* 8-bit input, 16-bit output */\n') opts.h_output.write("extern const uint16_t srgb_decode_8_to_16_table[256];") opts.h_output.write('\n') opts.h_output.write('static inline uint16_t\n') opts.h_output.write('srgb_decode_8_to_16(uint8_t v)\n') opts.h_output.write('{\n') opts.h_output.write(' return srgb_decode_8_to_16_table[v];\n') opts.h_output.write('}\n') opts.h_output.write('\n') opts.h_output.write('/* 8-bit input, 8-bit output */\n') opts.h_output.write("extern const uint8_t srgb_decode_8_to_8_table[256];\n") opts.h_output.write('\n') opts.h_output.write('static inline uint8_t\n') opts.h_output.write('srgb_decode_8_to_8(uint8_t v)\n') opts.h_output.write('{\n') opts.h_output.write(' return srgb_decode_8_to_8_table[v];\n') opts.h_output.write('}\n') opts.c_output.write('#include "srgb.h"\n') opts.c_output.write('\n') opts.c_output.write("const uint16_t srgb_decode_8_to_16_table[256] = {\n") for i in range(256): opts.c_output.write(f' {linear_table[i]},\n') opts.c_output.write('};\n') opts.c_output.write("const uint8_t srgb_decode_8_to_8_table[256] = {\n") for i in range(256): opts.c_output.write(f' {linear_table[i] >> 8},\n') opts.c_output.write('};\n') if __name__ == '__main__': sys.exit(main()) foot-1.21.0/search.c000066400000000000000000001253311476600145200141650ustar00rootroot00000000000000#include "search.h" #include #include #include #define LOG_MODULE "search" #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" #include "commands.h" #include "config.h" #include "extract.h" #include "grid.h" #include "input.h" #include "key-binding.h" #include "misc.h" #include "quirks.h" #include "render.h" #include "selection.h" #include "shm.h" #include "unicode-mode.h" #include "util.h" #include "xmalloc.h" /* * Ensures a "new" viewport doesn't contain any unallocated rows. * * This is done by first checking if the *first* row is NULL. If so, * we move the viewport *forward*, until the first row is non-NULL. At * this point, the entire viewport should be allocated rows only. * * If the first row already was non-NULL, we instead check the *last* * row, and if it is NULL, we move the viewport *backward* until the * last row is non-NULL. */ static int ensure_view_is_allocated(struct terminal *term, int new_view) { struct grid *grid = term->grid; int view_end = (new_view + term->rows - 1) & (grid->num_rows - 1); if (grid->rows[new_view] == NULL) { while (grid->rows[new_view] == NULL) new_view = (new_view + 1) & (grid->num_rows - 1); } else if (grid->rows[view_end] == NULL) { while (grid->rows[view_end] == NULL) { new_view--; if (new_view < 0) new_view += grid->num_rows; view_end = (new_view + term->rows - 1) & (grid->num_rows - 1); } } #if defined(_DEBUG) for (size_t r = 0; r < term->rows; r++) xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL); #endif return new_view; } static bool search_ensure_size(struct terminal *term, size_t wanted_size) { while (wanted_size >= term->search.sz) { size_t new_sz = term->search.sz == 0 ? 64 : term->search.sz * 2; char32_t *new_buf = realloc(term->search.buf, new_sz * sizeof(term->search.buf[0])); if (new_buf == NULL) { LOG_ERRNO("failed to resize search buffer"); return false; } term->search.buf = new_buf; term->search.sz = new_sz; } return true; } static bool has_wrapped_around_left(const struct terminal *term, int abs_row_no) { int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no); return rebased_row == term->grid->num_rows - 1 || term->grid->rows[abs_row_no] == NULL; } static bool has_wrapped_around_right(const struct terminal *term, int abs_row_no) { int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no); return rebased_row == 0; } static void search_cancel_keep_selection(struct terminal *term) { struct wl_window *win = term->window; wayl_win_subsurface_destroy(&win->search); if (term->search.len > 0) { free(term->search.last.buf); term->search.last.buf = term->search.buf; term->search.last.len = term->search.len; } else free(term->search.buf); term->search.buf = NULL; term->search.len = term->search.sz = 0; term->search.cursor = 0; term->search.match = (struct coord){-1, -1}; term->search.match_len = 0; term->is_searching = false; term->render.search_glyph_offset = 0; /* Reset IME state */ if (term_ime_is_enabled(term)) { term_ime_disable(term); term_ime_enable(term); } term_xcursor_update(term); render_refresh(term); } void search_begin(struct terminal *term) { LOG_DBG("search: begin"); search_cancel_keep_selection(term); selection_cancel(term); /* Reset IME state */ if (term_ime_is_enabled(term)) { term_ime_disable(term); term_ime_enable(term); } /* On-demand instantiate wayland surface */ bool ret = wayl_win_subsurface_new( term->window, &term->window->search, false); xassert(ret); const struct grid *grid = term->grid; term->search.original_view = grid->view; term->search.view_followed_offset = grid->view == grid->offset; term->is_searching = true; term->search.len = 0; term->search.sz = 64; term->search.buf = xmalloc(term->search.sz * sizeof(term->search.buf[0])); term->search.buf[0] = U'\0'; term_xcursor_update(term); render_refresh_search(term); } void search_cancel(struct terminal *term) { if (!term->is_searching) return; search_cancel_keep_selection(term); selection_cancel(term); } void search_selection_cancelled(struct terminal *term) { term->search.match = (struct coord){-1, -1}; term->search.match_len = 0; render_refresh_search(term); } static void search_update_selection(struct terminal *term, const struct range *match) { struct grid *grid = term->grid; int start_row = match->start.row; int start_col = match->start.col; int end_row = match->end.row; int end_col = match->end.col; xassert(start_row >= 0); xassert(start_row < grid->num_rows); bool move_viewport = true; int view_end = (grid->view + term->rows - 1) & (grid->num_rows - 1); if (view_end >= grid->view) { /* Viewport does *not* wrap around */ if (start_row >= grid->view && end_row <= view_end) move_viewport = false; } else { /* Viewport wraps */ if (start_row >= grid->view || end_row <= view_end) move_viewport = false; } if (move_viewport) { int rebased_new_view = grid_row_abs_to_sb(grid, term->rows, start_row); rebased_new_view -= term->rows / 2; rebased_new_view = min(max(rebased_new_view, 0), grid->num_rows - term->rows); const int old_view = grid->view; int new_view = grid_row_sb_to_abs(grid, term->rows, rebased_new_view); /* Scrollback may not be completely filled yet */ { const int mask = grid->num_rows - 1; while (grid->rows[new_view] == NULL) new_view = (new_view + 1) & mask; } #if defined(_DEBUG) /* Verify all to-be-visible rows have been allocated */ for (int r = 0; r < term->rows; r++) xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL); #endif #if defined(_DEBUG) { int rel_start_row = grid_row_abs_to_sb(grid, term->rows, start_row); int rel_view = grid_row_abs_to_sb(grid, term->rows, new_view); xassert(rel_view <= rel_start_row); xassert(rel_start_row < rel_view + term->rows); } #endif /* Update view */ grid->view = new_view; if (new_view != old_view) term_damage_view(term); } if (start_row != term->search.match.row || start_col != term->search.match.col || /* Pointer leave events trigger selection_finalize() :/ */ !term->selection.ongoing) { int selection_row = start_row - grid->view + grid->num_rows; selection_row &= grid->num_rows - 1; selection_start( term, start_col, selection_row, SELECTION_CHAR_WISE, false); term->search.match.row = start_row; term->search.match.col = start_col; } /* Update selection endpoint */ { int selection_row = end_row - grid->view + grid->num_rows; selection_row &= grid->num_rows - 1; selection_update(term, end_col, selection_row); } } static ssize_t matches_cell(const struct terminal *term, const struct cell *cell, size_t search_ofs) { assert(search_ofs < term->search.len); char32_t base = cell->wc; const struct composed *composed = NULL; if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) { composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); base = composed->chars[0]; } if (composed == NULL && base == 0 && term->search.buf[search_ofs] == U' ') return 1; if (c32ncasecmp(&base, &term->search.buf[search_ofs], 1) != 0) return -1; if (composed != NULL) { if (search_ofs + composed->count > term->search.len) return -1; for (size_t j = 1; j < composed->count; j++) { if (composed->chars[j] != term->search.buf[search_ofs + j]) return -1; } } return composed != NULL ? composed->count : 1; } static bool find_next(struct terminal *term, enum search_direction direction, struct coord abs_start, struct coord abs_end, struct range *match) { #define ROW_DEC(_r) ((_r) = ((_r) - 1 + grid->num_rows) & (grid->num_rows - 1)) #define ROW_INC(_r) ((_r) = ((_r) + 1) & (grid->num_rows - 1)) struct grid *grid = term->grid; const bool backward = direction != SEARCH_FORWARD; LOG_DBG("%s: start: %dx%d, end: %dx%d", backward ? "backward" : "forward", abs_start.row, abs_start.col, abs_end.row, abs_end.col); xassert(abs_start.row >= 0); xassert(abs_start.row < grid->num_rows); xassert(abs_start.col >= 0); xassert(abs_start.col < term->cols); xassert(abs_end.row >= 0); xassert(abs_end.row < grid->num_rows); xassert(abs_end.col >= 0); xassert(abs_end.col < term->cols); for (int match_start_row = abs_start.row, match_start_col = abs_start.col; ; backward ? ROW_DEC(match_start_row) : ROW_INC(match_start_row)) { const struct row *row = grid->rows[match_start_row]; if (row == NULL) { if (match_start_row == abs_end.row) break; continue; } for (; backward ? match_start_col >= 0 : match_start_col < term->cols; backward ? match_start_col-- : match_start_col++) { if (matches_cell(term, &row->cells[match_start_col], 0) < 0) { if (match_start_row == abs_end.row && match_start_col == abs_end.col) { break; } continue; } /* * Got a match on the first letter. Now we'll see if the * rest of the search buffer matches. */ LOG_DBG("search: initial match at row=%d, col=%d", match_start_row, match_start_col); int match_end_row = match_start_row; int match_end_col = match_start_col; const struct row *match_row = row; size_t match_len = 0; for (size_t i = 0; i < term->search.len;) { if (match_end_col >= term->cols) { ROW_INC(match_end_row); match_end_col = 0; match_row = grid->rows[match_end_row]; if (match_row == NULL) break; } if (match_row->cells[match_end_col].wc >= CELL_SPACER) { match_end_col++; continue; } ssize_t additional_chars = matches_cell( term, &match_row->cells[match_end_col], i); if (additional_chars < 0) break; i += additional_chars; match_len += additional_chars; match_end_col++; while (match_end_col < term->cols && match_row->cells[match_end_col].wc > CELL_SPACER) { match_end_col++; } } if (match_len != term->search.len) { /* Didn't match (completely) */ if (match_start_row == abs_end.row && match_start_col == abs_end.col) { break; } continue; } *match = (struct range){ .start = {match_start_col, match_start_row}, .end = {match_end_col - 1, match_end_row}, }; return true; } if (match_start_row == abs_end.row && match_start_col == abs_end.col) break; match_start_col = backward ? term->cols - 1 : 0; } return false; } static void search_find_next(struct terminal *term, enum search_direction direction) { struct grid *grid = term->grid; if (term->search.len == 0) { term->search.match = (struct coord){-1, -1}; term->search.match_len = 0; selection_cancel(term); return; } struct coord start = term->search.match; size_t len = term->search.match_len; xassert((len == 0 && start.row == -1 && start.col == -1) || (len > 0 && start.row >= 0 && start.col >= 0)); if (len == 0) { /* No previous match, start from the top, or bottom, of the scrollback */ switch (direction) { case SEARCH_FORWARD: start.row = grid_row_absolute_in_view(grid, 0); start.col = 0; break; case SEARCH_BACKWARD: case SEARCH_BACKWARD_SAME_POSITION: start.row = grid_row_absolute_in_view(grid, term->rows - 1); start.col = term->cols - 1; break; } } else { /* Continue from last match */ xassert(start.row >= 0); xassert(start.col >= 0); switch (direction) { case SEARCH_BACKWARD_SAME_POSITION: break; case SEARCH_BACKWARD: if (--start.col < 0) { start.col = term->cols - 1; start.row += grid->num_rows - 1; start.row &= grid->num_rows - 1; } break; case SEARCH_FORWARD: if (++start.col >= term->cols) { start.col = 0; start.row++; start.row &= grid->num_rows - 1; } break; } xassert(start.row >= 0); xassert(start.row < grid->num_rows); xassert(start.col >= 0); xassert(start.col < term->cols); } LOG_DBG( "update: %s: starting at row=%d col=%d " "(offset = %d, view = %d)", direction != SEARCH_FORWARD ? "backward" : "forward", start.row, start.col, grid->offset, grid->view); struct coord end = start; switch (direction) { case SEARCH_FORWARD: /* Search forward, until we reach the cell *before* current start */ if (--end.col < 0) { end.col = term->cols - 1; end.row += grid->num_rows - 1; end.row &= grid->num_rows - 1; } break; case SEARCH_BACKWARD: case SEARCH_BACKWARD_SAME_POSITION: /* Search backwards, until we reach the cell *after* current start */ if (++end.col >= term->cols) { end.col = 0; end.row++; end.row &= grid->num_rows - 1; } break; } struct range match; bool found = find_next(term, direction, start, end, &match); if (found) { LOG_DBG("primary match found at %dx%d", match.start.row, match.start.col); search_update_selection(term, &match); term->search.match = match.start; term->search.match_len = term->search.len; } else { LOG_DBG("no match"); term->search.match = (struct coord){-1, -1}; term->search.match_len = 0; selection_cancel(term); } #undef ROW_DEC } struct search_match_iterator search_matches_new_iter(struct terminal *term) { return (struct search_match_iterator){ .term = term, .start = {0, 0}, }; } struct range search_matches_next(struct search_match_iterator *iter) { struct terminal *term = iter->term; struct grid *grid = term->grid; if (term->search.match_len == 0) goto no_match; if (iter->start.row >= term->rows) goto no_match; xassert(iter->start.row >= 0); xassert(iter->start.row < term->rows); xassert(iter->start.col >= 0); xassert(iter->start.col < term->cols); struct coord abs_start = iter->start; abs_start.row = grid_row_absolute_in_view(grid, abs_start.row); struct coord abs_end = { term->cols - 1, grid_row_absolute_in_view(grid, term->rows - 1)}; /* BUG: matches *starting* outside the view, but ending *inside*, aren't matched */ struct range match; bool found = find_next(term, SEARCH_FORWARD, abs_start, abs_end, &match); if (!found) goto no_match; LOG_DBG("match at (absolute coordinates) %dx%d-%dx%d", match.start.row, match.start.col, match.end.row, match.end.col); /* Convert absolute row numbers back to view relative */ match.start.row = match.start.row - grid->view + grid->num_rows; match.start.row &= grid->num_rows - 1; match.end.row = match.end.row - grid->view + grid->num_rows; match.end.row &= grid->num_rows - 1; LOG_DBG("match at (view-local coordinates) %dx%d-%dx%d, view=%d", match.start.row, match.start.col, match.end.row, match.end.col, grid->view); /* Assert match end comes *after* the match start */ xassert(match.end.row > match.start.row || (match.end.row == match.start.row && match.end.col >= match.start.col)); /* Assert the match starts at, or after, the iterator position */ xassert(match.start.row > iter->start.row || (match.start.row == iter->start.row && match.start.col >= iter->start.col)); /* Continue at next column, next time */ iter->start.row = match.start.row; iter->start.col = match.start.col + 1; if (iter->start.col >= term->cols) { iter->start.col = 0; iter->start.row++; /* Overflow is caught in next iteration */ } xassert(iter->start.row >= 0); xassert(iter->start.row <= term->rows); xassert(iter->start.col >= 0); xassert(iter->start.col < term->cols); return match; no_match: iter->start.row = -1; iter->start.col = -1; return (struct range){{-1, -1}, {-1, -1}}; } static void add_wchars(struct terminal *term, char32_t *src, size_t count) { /* Strip non-printable characters */ for (size_t i = 0, j = 0, orig_count = count; i < orig_count; i++) { if (isc32print(src[i])) src[j++] = src[i]; else count--; } if (!search_ensure_size(term, term->search.len + count)) return; xassert(term->search.len + count < term->search.sz); memmove(&term->search.buf[term->search.cursor + count], &term->search.buf[term->search.cursor], (term->search.len - term->search.cursor) * sizeof(char32_t)); memcpy(&term->search.buf[term->search.cursor], src, count * sizeof(char32_t)); term->search.len += count; term->search.cursor += count; term->search.buf[term->search.len] = U'\0'; } void search_add_chars(struct terminal *term, const char *src, size_t count) { size_t chars = mbsntoc32(NULL, src, count, 0); if (chars == (size_t)-1) { LOG_ERRNO("failed to convert %.*s to Unicode", (int)count, src); return; } char32_t c32s[chars + 1]; mbsntoc32(c32s, src, count, chars); add_wchars(term, c32s, chars); } enum extend_direction {SEARCH_EXTEND_LEFT, SEARCH_EXTEND_RIGHT}; static bool coord_advance_left(const struct terminal *term, struct coord *pos, const struct row **row) { const struct grid *grid = term->grid; struct coord new_pos = *pos; if (--new_pos.col < 0) { new_pos.row = (new_pos.row - 1 + grid->num_rows) & (grid->num_rows - 1); new_pos.col = term->cols - 1; if (has_wrapped_around_left(term, new_pos.row)) return false; if (row != NULL) *row = grid->rows[new_pos.row]; } *pos = new_pos; return true; } static bool coord_advance_right(const struct terminal *term, struct coord *pos, const struct row **row) { const struct grid *grid = term->grid; struct coord new_pos = *pos; if (++new_pos.col >= term->cols) { new_pos.row = (new_pos.row + 1) & (grid->num_rows - 1); new_pos.col = 0; if (has_wrapped_around_right(term, new_pos.row)) return false; if (row != NULL) *row = grid->rows[new_pos.row]; } *pos = new_pos; return true; } static bool search_extend_find_char(const struct terminal *term, struct coord *target, enum extend_direction direction) { if (term->search.match_len == 0) return false; struct coord pos = direction == SEARCH_EXTEND_LEFT ? selection_get_start(term) : selection_get_end(term); xassert(pos.row >= 0); xassert(pos.row < term->grid->num_rows); *target = pos; const struct row *row = term->grid->rows[pos.row]; while (true) { switch (direction) { case SEARCH_EXTEND_LEFT: if (!coord_advance_left(term, &pos, &row)) return false; break; case SEARCH_EXTEND_RIGHT: if (!coord_advance_right(term, &pos, &row)) return false; break; } const char32_t wc = row->cells[pos.col].wc; if (wc >= CELL_SPACER || wc == U'\0') continue; *target = pos; return true; } } static bool search_extend_find_char_left(const struct terminal *term, struct coord *target) { return search_extend_find_char(term, target, SEARCH_EXTEND_LEFT); } static bool search_extend_find_char_right(const struct terminal *term, struct coord *target) { return search_extend_find_char(term, target, SEARCH_EXTEND_RIGHT); } static bool search_extend_find_word(const struct terminal *term, bool spaces_only, struct coord *target, enum extend_direction direction) { if (term->search.match_len == 0) return false; struct grid *grid = term->grid; struct coord pos = direction == SEARCH_EXTEND_LEFT ? selection_get_start(term) : selection_get_end(term); xassert(pos.row >= 0); xassert(pos.row < grid->num_rows); *target = pos; /* First character to consider is the *next* character */ switch (direction) { case SEARCH_EXTEND_LEFT: if (!coord_advance_left(term, &pos, NULL)) return false; break; case SEARCH_EXTEND_RIGHT: if (!coord_advance_right(term, &pos, NULL)) return false; break; } xassert(pos.row >= 0); xassert(pos.row < grid->num_rows); xassert(grid->rows[pos.row] != NULL); /* Find next word boundary */ switch (direction) { case SEARCH_EXTEND_LEFT: selection_find_word_boundary_left(term, &pos, spaces_only); break; case SEARCH_EXTEND_RIGHT: selection_find_word_boundary_right(term, &pos, spaces_only, false); break; } *target = pos; return true; } static bool search_extend_find_word_left(const struct terminal *term, bool spaces_only, struct coord *target) { return search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_LEFT); } static bool search_extend_find_word_right(const struct terminal *term, bool spaces_only, struct coord *target) { return search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_RIGHT); } static bool search_extend_find_line(const struct terminal *term, struct coord *target, enum extend_direction direction) { if (term->search.match_len == 0) return false; struct coord pos = direction == SEARCH_EXTEND_LEFT ? selection_get_start(term) : selection_get_end(term); xassert(pos.row >= 0); xassert(pos.row < term->grid->num_rows); *target = pos; const struct grid *grid = term->grid; switch (direction) { case SEARCH_EXTEND_LEFT: pos.row = (pos.row - 1 + grid->num_rows) & (grid->num_rows - 1); if (has_wrapped_around_left(term, pos.row)) return false; break; case SEARCH_EXTEND_RIGHT: pos.row = (pos.row + 1) & (grid->num_rows - 1); if (has_wrapped_around_right(term, pos.row)) return false; break; } *target = pos; return true; } static bool search_extend_find_line_up(const struct terminal *term, struct coord *target) { return search_extend_find_line(term, target, SEARCH_EXTEND_LEFT); } static bool search_extend_find_line_down(const struct terminal *term, struct coord *target) { return search_extend_find_line(term, target, SEARCH_EXTEND_RIGHT); } static void search_extend_left(struct terminal *term, const struct coord *target) { if (term->search.match_len == 0) return; const struct coord last_coord = selection_get_start(term); struct coord pos = *target; const struct row *row = term->grid->rows[pos.row]; const bool move_cursor = term->search.cursor != 0; struct extraction_context *ctx = extract_begin(SELECTION_NONE, false); if (ctx == NULL) return; while (pos.col != last_coord.col || pos.row != last_coord.row) { if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx)) break; if (!coord_advance_right(term, &pos, &row)) break; } char32_t *new_text; size_t new_len; if (!extract_finish_wide(ctx, &new_text, &new_len)) return; if (!search_ensure_size(term, term->search.len + new_len)) return; memmove(&term->search.buf[new_len], &term->search.buf[0], term->search.len * sizeof(term->search.buf[0])); size_t actually_copied = 0; for (size_t i = 0; i < new_len; i++) { if (new_text[i] == U'\n') { /* extract() adds newlines, which we never match against */ continue; } term->search.buf[actually_copied++] = new_text[i]; term->search.len++; } xassert(actually_copied <= new_len); if (actually_copied < new_len) { memmove( &term->search.buf[actually_copied], &term->search.buf[new_len], (term->search.len - actually_copied) * sizeof(term->search.buf[0])); } term->search.buf[term->search.len] = U'\0'; free(new_text); if (move_cursor) term->search.cursor += actually_copied; struct range match = {.start = *target, .end = selection_get_end(term)}; search_update_selection(term, &match); term->search.match_len = term->search.len; } static void search_extend_right(struct terminal *term, const struct coord *target) { if (term->search.match_len == 0) return; struct coord pos = selection_get_end(term); const struct row *row = term->grid->rows[pos.row]; const bool move_cursor = term->search.cursor == term->search.len; struct extraction_context *ctx = extract_begin(SELECTION_NONE, false); if (ctx == NULL) return; do { if (!coord_advance_right(term, &pos, &row)) break; if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx)) break; } while (pos.col != target->col || pos.row != target->row); char32_t *new_text; size_t new_len; if (!extract_finish_wide(ctx, &new_text, &new_len)) return; if (!search_ensure_size(term, term->search.len + new_len)) return; for (size_t i = 0; i < new_len; i++) { if (new_text[i] == U'\n') { /* extract() adds newlines, which we never match against */ continue; } term->search.buf[term->search.len++] = new_text[i]; } term->search.buf[term->search.len] = U'\0'; free(new_text); if (move_cursor) term->search.cursor = term->search.len; struct range match = {.start = term->search.match, .end = *target}; search_update_selection(term, &match); term->search.match_len = term->search.len; } static size_t distance_next_word(const struct terminal *term) { size_t cursor = term->search.cursor; /* First eat non-whitespace. This is the word we're skipping past */ while (cursor < term->search.len) { if (isc32space(term->search.buf[cursor++])) break; } xassert(cursor == term->search.len || isc32space(term->search.buf[cursor - 1])); /* Now skip past whitespace, so that we end up at the beginning of * the next word */ while (cursor < term->search.len) { if (!isc32space(term->search.buf[cursor++])) break; } xassert(cursor == term->search.len || !isc32space(term->search.buf[cursor - 1])); if (cursor < term->search.len && !isc32space(term->search.buf[cursor])) cursor--; return cursor - term->search.cursor; } static size_t distance_prev_word(const struct terminal *term) { int cursor = term->search.cursor; /* First, eat whitespace prefix */ while (cursor > 0) { if (!isc32space(term->search.buf[--cursor])) break; } xassert(cursor == 0 || !isc32space(term->search.buf[cursor])); /* Now eat non-whitespace. This is the word we're skipping past */ while (cursor > 0) { if (isc32space(term->search.buf[--cursor])) break; } xassert(cursor == 0 || isc32space(term->search.buf[cursor])); if (cursor > 0 && isc32space(term->search.buf[cursor])) cursor++; return term->search.cursor - cursor; } static void from_clipboard_cb(char *text, size_t size, void *user) { struct terminal *term = user; search_add_chars(term, text, size); } static void from_clipboard_done(void *user) { struct terminal *term = user; LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf); search_find_next(term, SEARCH_BACKWARD_SAME_POSITION); render_refresh_search(term); } static bool execute_binding(struct seat *seat, struct terminal *term, const struct key_binding *binding, uint32_t serial, bool *update_search_result, enum search_direction *direction, bool *redraw) { *update_search_result = *redraw = false; const enum bind_action_search action = binding->action; struct grid *grid = term->grid; switch (action) { case BIND_ACTION_SEARCH_NONE: return false; case BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE: if (term->grid == &term->normal) { cmd_scrollback_up(term, term->rows); return true; } return false; case BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE: if (term->grid == &term->normal) { cmd_scrollback_up(term, max(term->rows / 2, 1)); return true; } break; case BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE: if (term->grid == &term->normal) { cmd_scrollback_up(term, 1); return true; } break; case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE: if (term->grid == &term->normal) { cmd_scrollback_down(term, term->rows); return true; } return false; case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE: if (term->grid == &term->normal) { cmd_scrollback_down(term, max(term->rows / 2, 1)); return true; } break; case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE: if (term->grid == &term->normal) { cmd_scrollback_down(term, 1); return true; } break; case BIND_ACTION_SEARCH_SCROLLBACK_HOME: if (term->grid == &term->normal) { cmd_scrollback_up(term, term->grid->num_rows); return true; } break; case BIND_ACTION_SEARCH_SCROLLBACK_END: if (term->grid == &term->normal) { cmd_scrollback_down(term, term->grid->num_rows); return true; } break; case BIND_ACTION_SEARCH_CANCEL: if (term->search.view_followed_offset) grid->view = grid->offset; else { grid->view = ensure_view_is_allocated( term, term->search.original_view); } term_damage_view(term); search_cancel(term); return true; case BIND_ACTION_SEARCH_COMMIT: selection_finalize(seat, term, serial); search_cancel_keep_selection(term); return true; case BIND_ACTION_SEARCH_FIND_PREV: if (term->search.last.buf != NULL && term->search.len == 0) { add_wchars(term, term->search.last.buf, term->search.last.len); free(term->search.last.buf); term->search.last.buf = NULL; term->search.last.len = 0; } *direction = SEARCH_BACKWARD; *update_search_result = *redraw = true; return true; case BIND_ACTION_SEARCH_FIND_NEXT: if (term->search.last.buf != NULL && term->search.len == 0) { add_wchars(term, term->search.last.buf, term->search.last.len); free(term->search.last.buf); term->search.last.buf = NULL; term->search.last.len = 0; } *direction = SEARCH_FORWARD; *update_search_result = *redraw = true; return true; case BIND_ACTION_SEARCH_EDIT_LEFT: if (term->search.cursor > 0) { term->search.cursor--; *redraw = true; } return true; case BIND_ACTION_SEARCH_EDIT_LEFT_WORD: { size_t diff = distance_prev_word(term); term->search.cursor -= diff; xassert(term->search.cursor <= term->search.len); if (diff > 0) *redraw = true; return true; } case BIND_ACTION_SEARCH_EDIT_RIGHT: if (term->search.cursor < term->search.len) { term->search.cursor++; *redraw = true; } return true; case BIND_ACTION_SEARCH_EDIT_RIGHT_WORD: { size_t diff = distance_next_word(term); term->search.cursor += diff; xassert(term->search.cursor <= term->search.len); if (diff > 0) *redraw = true; return true; } case BIND_ACTION_SEARCH_EDIT_HOME: if (term->search.cursor != 0) { term->search.cursor = 0; *redraw = true; } return true; case BIND_ACTION_SEARCH_EDIT_END: if (term->search.cursor != term->search.len) { term->search.cursor = term->search.len; *redraw = true; } return true; case BIND_ACTION_SEARCH_DELETE_PREV: if (term->search.cursor > 0) { memmove( &term->search.buf[term->search.cursor - 1], &term->search.buf[term->search.cursor], (term->search.len - term->search.cursor) * sizeof(char32_t)); term->search.cursor--; term->search.buf[--term->search.len] = U'\0'; *update_search_result = *redraw = true; } return true; case BIND_ACTION_SEARCH_DELETE_PREV_WORD: { size_t diff = distance_prev_word(term); size_t old_cursor = term->search.cursor; size_t new_cursor = old_cursor - diff; if (diff > 0) { memmove(&term->search.buf[new_cursor], &term->search.buf[old_cursor], (term->search.len - old_cursor) * sizeof(char32_t)); term->search.len -= diff; term->search.cursor = new_cursor; *update_search_result = *redraw = true; } return true; } case BIND_ACTION_SEARCH_DELETE_NEXT: if (term->search.cursor < term->search.len) { memmove( &term->search.buf[term->search.cursor], &term->search.buf[term->search.cursor + 1], (term->search.len - term->search.cursor - 1) * sizeof(char32_t)); term->search.buf[--term->search.len] = U'\0'; *update_search_result = *redraw = true; } return true; case BIND_ACTION_SEARCH_DELETE_NEXT_WORD: { size_t diff = distance_next_word(term); size_t cursor = term->search.cursor; if (diff > 0) { memmove(&term->search.buf[cursor], &term->search.buf[cursor + diff], (term->search.len - (cursor + diff)) * sizeof(char32_t)); term->search.len -= diff; *update_search_result = *redraw = true; } return true; } case BIND_ACTION_SEARCH_DELETE_TO_START: { if (term->search.cursor > 0) { memmove(&term->search.buf[0], &term->search.buf[term->search.cursor], (term->search.len - term->search.cursor) * sizeof(char32_t)); term->search.len -= term->search.cursor; term->search.cursor = 0; *update_search_result = *redraw = true; } return true; } case BIND_ACTION_SEARCH_DELETE_TO_END: { if (term->search.cursor < term->search.len) { term->search.buf[term->search.cursor] = '\0'; term->search.len = term->search.cursor; *update_search_result = *redraw = true; } return true; } case BIND_ACTION_SEARCH_EXTEND_CHAR: { struct coord target; if (search_extend_find_char_right(term, &target)) { search_extend_right(term, &target); *update_search_result = false; *redraw = true; } return true; } case BIND_ACTION_SEARCH_EXTEND_WORD: { struct coord target; if (search_extend_find_word_right(term, false, &target)) { search_extend_right(term, &target); *update_search_result = false; *redraw = true; } return true; } case BIND_ACTION_SEARCH_EXTEND_WORD_WS: { struct coord target; if (search_extend_find_word_right(term, true, &target)) { search_extend_right(term, &target); *update_search_result = false; *redraw = true; } return true; } case BIND_ACTION_SEARCH_EXTEND_LINE_DOWN: { struct coord target; if (search_extend_find_line_down(term, &target)) { search_extend_right(term, &target); *update_search_result = false; *redraw = true; } return true; } case BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR: { struct coord target; if (search_extend_find_char_left(term, &target)) { search_extend_left(term, &target); *update_search_result = false; *redraw = true; } return true; } case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD: { struct coord target; if (search_extend_find_word_left(term, false, &target)) { search_extend_left(term, &target); *update_search_result = false; *redraw = true; } return true; } case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS: { struct coord target; if (search_extend_find_word_left(term, true, &target)) { search_extend_left(term, &target); *update_search_result = false; *redraw = true; } return true; } case BIND_ACTION_SEARCH_EXTEND_LINE_UP: { struct coord target; if (search_extend_find_line_up(term, &target)) { search_extend_left(term, &target); *update_search_result = false; *redraw = true; } return true; } case BIND_ACTION_SEARCH_CLIPBOARD_PASTE: text_from_clipboard( seat, term, &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); *update_search_result = *redraw = true; return true; case BIND_ACTION_SEARCH_UNICODE_INPUT: unicode_mode_activate(term); return true; case BIND_ACTION_SEARCH_COUNT: BUG("Invalid action type"); return true; } BUG("Unhandled action type"); return false; } void search_input(struct seat *seat, struct terminal *term, const struct key_binding_set *bindings, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, const xkb_keysym_t *raw_syms, size_t raw_count, uint32_t serial) { LOG_DBG("search: input: sym=%d/0x%x, mods=0x%08x, consumed=0x%08x", sym, sym, mods, consumed); enum xkb_compose_status compose_status = seat->kbd.xkb_compose_state != NULL ? xkb_compose_state_get_status(seat->kbd.xkb_compose_state) : XKB_COMPOSE_NOTHING; enum search_direction search_direction = SEARCH_BACKWARD_SAME_POSITION; bool update_search_result = false; bool redraw = false; /* * Key bindings */ /* Match untranslated symbols */ tll_foreach(bindings->search, it) { const struct key_binding *bind = &it->item; if (bind->mods != mods || bind->mods == 0) continue; for (size_t i = 0; i < raw_count; i++) { if (bind->k.sym == raw_syms[i]) { if (execute_binding(seat, term, bind, serial, &update_search_result, &search_direction, &redraw)) { goto update_search; } return; } } } /* Match translated symbol */ tll_foreach(bindings->search, it) { const struct key_binding *bind = &it->item; if (bind->k.sym == sym && bind->mods == (mods & ~consumed)) { if (execute_binding(seat, term, bind, serial, &update_search_result, &search_direction, &redraw)) { goto update_search; } return; } } /* Match raw key code */ tll_foreach(bindings->search, it) { const struct key_binding *bind = &it->item; if (bind->mods != mods || bind->mods == 0) continue; tll_foreach(bind->k.key_codes, code) { if (code->item == key) { if (execute_binding(seat, term, bind, serial, &update_search_result, &search_direction, &redraw)) { goto update_search; } return; } } } uint8_t buf[64] = {0}; int count = 0; if (compose_status == XKB_COMPOSE_COMPOSED) { count = xkb_compose_state_get_utf8( seat->kbd.xkb_compose_state, (char *)buf, sizeof(buf)); xkb_compose_state_reset(seat->kbd.xkb_compose_state); } else if (compose_status == XKB_COMPOSE_CANCELLED) { count = 0; } else { count = xkb_state_key_get_utf8( seat->kbd.xkb_state, key, (char *)buf, sizeof(buf)); } update_search_result = redraw = count > 0; search_direction = SEARCH_BACKWARD_SAME_POSITION; if (count == 0) return; search_add_chars(term, (const char *)buf, count); update_search: LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf); if (update_search_result) search_find_next(term, search_direction); if (redraw) render_refresh_search(term); } foot-1.21.0/search.h000066400000000000000000000014551476600145200141720ustar00rootroot00000000000000#pragma once #include #include "key-binding.h" #include "terminal.h" void search_begin(struct terminal *term); void search_cancel(struct terminal *term); void search_input( struct seat *seat, struct terminal *term, const struct key_binding_set *bindings, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, const xkb_keysym_t *raw_syms, size_t raw_count, uint32_t serial); void search_add_chars(struct terminal *term, const char *text, size_t len); void search_selection_cancelled(struct terminal *term); struct search_match_iterator { struct terminal *term; struct coord start; }; struct search_match_iterator search_matches_new_iter(struct terminal *term); struct range search_matches_next(struct search_match_iterator *iter); foot-1.21.0/selection.c000066400000000000000000002517571476600145200147210ustar00rootroot00000000000000#include "selection.h" #include #include #include #include #include #include #include #include #define LOG_MODULE "selection" #define LOG_ENABLE_DBG 0 #include "log.h" #include "async.h" #include "char32.h" #include "commands.h" #include "config.h" #include "extract.h" #include "grid.h" #include "misc.h" #include "render.h" #include "search.h" #include "uri.h" #include "util.h" #include "vt.h" #include "xmalloc.h" static const char *const mime_type_map[] = { [DATA_OFFER_MIME_UNSET] = NULL, [DATA_OFFER_MIME_TEXT_PLAIN] = "text/plain", [DATA_OFFER_MIME_TEXT_UTF8] = "text/plain;charset=utf-8", [DATA_OFFER_MIME_URI_LIST] = "text/uri-list", [DATA_OFFER_MIME_TEXT_TEXT] = "TEXT", [DATA_OFFER_MIME_TEXT_STRING] = "STRING", [DATA_OFFER_MIME_TEXT_UTF8_STRING] = "UTF8_STRING", }; static inline struct coord bounded(const struct grid *grid, struct coord coord) { coord.row &= grid->num_rows - 1; return coord; } struct coord selection_get_start(const struct terminal *term) { if (term->selection.coords.start.row < 0) return term->selection.coords.start; return bounded(term->grid, term->selection.coords.start); } struct coord selection_get_end(const struct terminal *term) { if (term->selection.coords.end.row < 0) return term->selection.coords.end; return bounded(term->grid, term->selection.coords.end); } bool selection_on_rows(const struct terminal *term, int row_start, int row_end) { xassert(term->selection.coords.end.row >= 0); LOG_DBG("on rows: %d-%d, range: %d-%d (offset=%d)", term->selection.coords.start.row, term->selection.coords.end.row, row_start, row_end, term->grid->offset); row_start += term->grid->offset; row_end += term->grid->offset; xassert(row_end >= row_start); const struct coord *start = &term->selection.coords.start; const struct coord *end = &term->selection.coords.end; const struct grid *grid = term->grid; const int sb_start = grid->offset + term->rows; /* Use scrollback relative coords when checking for overlap */ const int rel_row_start = grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, row_start); const int rel_row_end = grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, row_end); int rel_sel_start = grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, start->row); int rel_sel_end = grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, end->row); if (rel_sel_start > rel_sel_end) { int tmp = rel_sel_start; rel_sel_start = rel_sel_end; rel_sel_end = tmp; } if ((rel_row_start <= rel_sel_start && rel_row_end >= rel_sel_start) || (rel_row_start <= rel_sel_end && rel_row_end >= rel_sel_end)) { /* The range crosses one of the selection boundaries */ return true; } if (rel_row_start >= rel_sel_start && rel_row_end <= rel_sel_end) return true; return false; } void selection_scroll_up(struct terminal *term, int rows) { xassert(term->selection.coords.end.row >= 0); const int rel_row_start = grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.start.row); const int rel_row_end = grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.end.row); const int actual_start = min(rel_row_start, rel_row_end); if (actual_start - rows < 0) { /* Part of the selection will be scrolled out, cancel it */ selection_cancel(term); } } void selection_scroll_down(struct terminal *term, int rows) { xassert(term->selection.coords.end.row >= 0); const struct grid *grid = term->grid; const struct range *sel = &term->selection.coords; const int screen_end = grid_row_abs_to_sb(grid, term->rows, grid->offset + term->rows - 1); const int rel_row_start = grid_row_abs_to_sb(term->grid, term->rows, sel->start.row); const int rel_row_end = grid_row_abs_to_sb(term->grid, term->rows, sel->end.row); const int actual_end = max(rel_row_start, rel_row_end); if (actual_end > screen_end - rows) { /* Part of the selection will be scrolled out, cancel it */ selection_cancel(term); } } void selection_view_up(struct terminal *term, int new_view) { if (likely(term->selection.coords.start.row < 0)) return; if (likely(new_view < term->grid->view)) return; term->selection.coords.start.row += term->grid->num_rows; if (term->selection.coords.end.row >= 0) term->selection.coords.end.row += term->grid->num_rows; } void selection_view_down(struct terminal *term, int new_view) { if (likely(term->selection.coords.start.row < 0)) return; if (likely(new_view > term->grid->view)) return; term->selection.coords.start.row &= term->grid->num_rows - 1; if (term->selection.coords.end.row >= 0) term->selection.coords.end.row &= term->grid->num_rows - 1; } static void foreach_selected_normal( struct terminal *term, struct coord _start, struct coord _end, bool (*cb)(struct terminal *term, struct row *row, struct cell *cell, int row_no, int col, void *data), void *data) { const struct coord *start = &_start; const struct coord *end = &_end; const int grid_rows = term->grid->num_rows; /* Start/end rows, relative to the scrollback start */ /* Start/end rows, relative to the scrollback start */ const int rel_start_row = grid_row_abs_to_sb(term->grid, term->rows, start->row); const int rel_end_row = grid_row_abs_to_sb(term->grid, term->rows, end->row); int start_row, end_row; int start_col, end_col; if (rel_start_row < rel_end_row) { start_row = start->row; start_col = start->col; end_row = end->row; end_col = end->col; } else if (rel_start_row > rel_end_row) { start_row = end->row; start_col = end->col; end_row = start->row; end_col = start->col; } else { start_row = end_row = start->row; start_col = min(start->col, end->col); end_col = max(start->col, end->col); } start_row &= (grid_rows - 1); end_row &= (grid_rows - 1); for (int r = start_row; r != end_row; r = (r + 1) & (grid_rows - 1)) { struct row *row = term->grid->rows[r]; xassert(row != NULL); for (int c = start_col; c <= term->cols - 1; c++) { if (!cb(term, row, &row->cells[c], r, c, data)) return; } start_col = 0; } /* Last, partial row */ struct row *row = term->grid->rows[end_row]; xassert(row != NULL); for (int c = start_col; c <= end_col; c++) { if (!cb(term, row, &row->cells[c], end_row, c, data)) return; } } static void foreach_selected_block( struct terminal *term, struct coord _start, struct coord _end, bool (*cb)(struct terminal *term, struct row *row, struct cell *cell, int row_no, int col, void *data), void *data) { const struct coord *start = &_start; const struct coord *end = &_end; const int grid_rows = term->grid->num_rows; /* Start/end rows, relative to the scrollback start */ const int rel_start_row = grid_row_abs_to_sb(term->grid, term->rows, start->row); const int rel_end_row = grid_row_abs_to_sb(term->grid, term->rows, end->row); struct coord top_left = { .row = (rel_start_row < rel_end_row ? start->row : end->row) & (grid_rows - 1), .col = min(start->col, end->col), }; struct coord bottom_right = { .row = (rel_start_row > rel_end_row ? start->row : end->row) & (grid_rows - 1), .col = max(start->col, end->col), }; int r = top_left.row; while (true) { struct row *row = term->grid->rows[r]; xassert(row != NULL); for (int c = top_left.col; c <= bottom_right.col; c++) { if (!cb(term, row, &row->cells[c], r, c, data)) return; } if (r == bottom_right.row) break; r++; r &= grid_rows - 1; } } static void foreach_selected( struct terminal *term, struct coord start, struct coord end, bool (*cb)(struct terminal *term, struct row *row, struct cell *cell, int row_no, int col, void *data), void *data) { switch (term->selection.kind) { case SELECTION_CHAR_WISE: case SELECTION_WORD_WISE: case SELECTION_QUOTE_WISE: case SELECTION_LINE_WISE: foreach_selected_normal(term, start, end, cb, data); return; case SELECTION_BLOCK: foreach_selected_block(term, start, end, cb, data); return; case SELECTION_NONE: break; } BUG("Invalid selection kind"); } static bool extract_one_const_wrapper(struct terminal *term, struct row *row, struct cell *cell, int row_no, int col, void *data) { return extract_one(term, row, cell, col, data); } char * selection_to_text(const struct terminal *term) { if (term->selection.coords.end.row == -1) return NULL; struct extraction_context *ctx = extract_begin(term->selection.kind, true); if (ctx == NULL) return NULL; foreach_selected( (struct terminal *)term, term->selection.coords.start, term->selection.coords.end, &extract_one_const_wrapper, ctx); char *text; return extract_finish(ctx, &text, NULL) ? text : NULL; } /* Coordinates are in *absolute* row numbers (NOT view local) */ void selection_find_word_boundary_left(const struct terminal *term, struct coord *pos, bool spaces_only) { const struct grid *grid = term->grid; xassert(pos->col >= 0); xassert(pos->col < term->cols); xassert(pos->row >= 0); pos->row &= grid->num_rows - 1; const struct row *r = grid->rows[pos->row]; char32_t c = r->cells[pos->col].wc; while (c >= CELL_SPACER) { xassert(pos->col > 0); if (pos->col == 0) return; pos->col--; c = r->cells[pos->col].wc; } if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI) c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0]; bool initial_is_space = c == 0 || isc32space(c); bool initial_is_delim = !initial_is_space && !isword(c, spaces_only, term->conf->word_delimiters); bool initial_is_word = c != 0 && isword(c, spaces_only, term->conf->word_delimiters); while (true) { int next_col = pos->col - 1; int next_row = pos->row; const struct row *row = grid->rows[next_row]; /* Linewrap */ if (next_col < 0) { next_col = term->cols - 1; next_row = (next_row - 1 + grid->num_rows) & (grid->num_rows - 1); if (grid_row_abs_to_sb(grid, term->rows, next_row) == term->grid->num_rows - 1 || grid->rows[next_row] == NULL) { /* Scrollback wrap-around */ break; } row = grid->rows[next_row]; if (row->linebreak) { /* Hard linebreak, treat as space. I.e. break selection */ break; } } c = row->cells[next_col].wc; while (c >= CELL_SPACER) { xassert(next_col > 0); if (--next_col < 0) return; c = row->cells[next_col].wc; } if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI) c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0]; bool is_space = c == 0 || isc32space(c); bool is_delim = !is_space && !isword(c, spaces_only, term->conf->word_delimiters); bool is_word = c != 0 && isword(c, spaces_only, term->conf->word_delimiters); if (initial_is_space && !is_space) break; if (initial_is_delim && !is_delim) break; if (initial_is_word && !is_word) break; pos->col = next_col; pos->row = next_row; } } /* Coordinates are in *absolute* row numbers (NOT view local) */ void selection_find_word_boundary_right(const struct terminal *term, struct coord *pos, bool spaces_only, bool stop_on_space_to_word_boundary) { const struct grid *grid = term->grid; xassert(pos->col >= 0); xassert(pos->col < term->cols); xassert(pos->row >= 0); pos->row &= grid->num_rows - 1; const struct row *r = grid->rows[pos->row]; char32_t c = r->cells[pos->col].wc; while (c >= CELL_SPACER) { xassert(pos->col > 0); if (pos->col == 0) return; pos->col--; c = r->cells[pos->col].wc; } if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI) c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0]; bool initial_is_space = c == 0 || isc32space(c); bool initial_is_delim = !initial_is_space && !isword(c, spaces_only, term->conf->word_delimiters); bool initial_is_word = c != 0 && isword(c, spaces_only, term->conf->word_delimiters); bool have_seen_word = initial_is_word; while (true) { int next_col = pos->col + 1; int next_row = pos->row; const struct row *row = term->grid->rows[next_row]; /* Linewrap */ if (next_col >= term->cols) { if (row->linebreak) { /* Hard linebreak, treat as space. I.e. break selection */ break; } next_col = 0; next_row = (next_row + 1) & (grid->num_rows - 1); if (grid_row_abs_to_sb(grid, term->rows, next_row) == 0) { /* Scrollback wrap-around */ break; } row = grid->rows[next_row]; } c = row->cells[next_col].wc; while (c >= CELL_SPACER) { if (++next_col >= term->cols) { next_col = 0; if (++next_row >= term->rows) return; } c = row->cells[next_col].wc; } if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI) c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0]; bool is_space = c == 0 || isc32space(c); bool is_delim = !is_space && !isword(c, spaces_only, term->conf->word_delimiters); bool is_word = c != 0 && isword(c, spaces_only, term->conf->word_delimiters); if (stop_on_space_to_word_boundary) { if (initial_is_space && !is_space) break; if (initial_is_delim && !is_delim) break; } else { if (initial_is_space && ((have_seen_word && is_space) || is_delim)) break; if (initial_is_delim && ((have_seen_word && is_delim) || is_space)) break; } if (initial_is_word && !is_word) break; have_seen_word = is_word; pos->col = next_col; pos->row = next_row; } } static bool selection_find_quote_left(struct terminal *term, struct coord *pos, char32_t *quote_char) { const struct row *row = grid_row_in_view(term->grid, pos->row); char32_t wc = row->cells[pos->col].wc; if (*quote_char == '\0' ? (wc == '"' || wc == '\'') : wc == *quote_char) { return false; } int next_row = pos->row; int next_col = pos->col; while (true) { if (--next_col < 0) { next_col = term->cols - 1; if (--next_row < 0) return false; row = grid_row_in_view(term->grid, next_row); if (row->linebreak) return false; } wc = row->cells[next_col].wc; if (*quote_char == '\0' ? (wc == '"' || wc == '\'') : wc == *quote_char) { pos->row = next_row; pos->col = next_col + 1; xassert(pos->col < term->cols); *quote_char = wc; return true; } } } static bool selection_find_quote_right(struct terminal *term, struct coord *pos, char32_t quote_char) { if (quote_char == '\0') return false; const struct row *row = grid_row_in_view(term->grid, pos->row); char32_t wc = row->cells[pos->col].wc; if (wc == quote_char) return false; int next_row = pos->row; int next_col = pos->col; while (true) { if (++next_col >= term->cols) { next_col = 0; if (++next_row >= term->rows) return false; if (row->linebreak) return false; row = grid_row_in_view(term->grid, next_row); } wc = row->cells[next_col].wc; if (wc == quote_char) { pos->row = next_row; pos->col = next_col - 1; xassert(pos->col >= 0); return true; } } } static void selection_find_line_boundary_left(struct terminal *term, struct coord *pos) { int next_row = pos->row; pos->col = 0; while (true) { if (--next_row < 0) return; const struct row *row = grid_row_in_view(term->grid, next_row); assert(row != NULL); if (row->linebreak) return; pos->col = 0; pos->row = next_row; } } static void selection_find_line_boundary_right(struct terminal *term, struct coord *pos) { int next_row = pos->row; pos->col = term->cols - 1; while (true) { const struct row *row = grid_row_in_view(term->grid, next_row); assert(row != NULL); if (row->linebreak) return; if (++next_row >= term->rows) return; pos->col = term->cols - 1; pos->row = next_row; } } void selection_start(struct terminal *term, int col, int row, enum selection_kind kind, bool spaces_only) { selection_cancel(term); LOG_DBG("%s selection started at %d,%d", kind == SELECTION_CHAR_WISE ? "character-wise" : kind == SELECTION_WORD_WISE ? "word-wise" : kind == SELECTION_QUOTE_WISE ? "quote-wise" : kind == SELECTION_LINE_WISE ? "line-wise" : kind == SELECTION_BLOCK ? "block" : "", row, col); term->selection.kind = kind; term->selection.ongoing = true; term->selection.spaces_only = spaces_only; switch (kind) { case SELECTION_CHAR_WISE: case SELECTION_BLOCK: term->selection.coords.start = (struct coord){col, term->grid->view + row}; term->selection.coords.end = (struct coord){-1, -1}; term->selection.pivot.start = term->selection.coords.start; term->selection.pivot.end = term->selection.coords.end; break; case SELECTION_WORD_WISE: { struct coord start = {col, term->grid->view + row}; struct coord end = {col, term->grid->view + row}; selection_find_word_boundary_left(term, &start, spaces_only); selection_find_word_boundary_right(term, &end, spaces_only, true); term->selection.coords.start = start; term->selection.pivot.start = term->selection.coords.start; term->selection.pivot.end = end; /* * FIXME: go through selection.c and make sure all public * functions use the *same* coordinate system... * * selection_find_word_boundary*() uses absolute row numbers, * while selection_update(), and pretty much all others, use * view-local. */ selection_update(term, end.col, end.row - term->grid->view); break; } case SELECTION_QUOTE_WISE: { struct coord start = {col, row}, end = {col, row}; char32_t quote_char = '\0'; bool found_left = selection_find_quote_left(term, &start, "e_char); bool found_right = selection_find_quote_right(term, &end, quote_char); if (found_left && !found_right) { xassert(quote_char != '\0'); /* * Try to flip the quote character we're looking for. * * This lets us handle things like: * * "nested 'quotes are fun', right" * * In the example above, starting the selection at * "right", will otherwise not match. find-left will find * the single quote, causing find-right to fail. * * By flipping the quote-character, and re-trying, we * find-left will find the starting double quote, letting * find-right succeed as well. */ if (quote_char == '\'') quote_char = '"'; else if (quote_char == '"') quote_char = '\''; found_left = selection_find_quote_left(term, &start, "e_char); found_right = selection_find_quote_right(term, &end, quote_char); } if (found_left && found_right) { term->selection.coords.start = (struct coord){ start.col, term->grid->view + start.row}; term->selection.pivot.start = term->selection.coords.start; term->selection.pivot.end = (struct coord){end.col, term->grid->view + end.row}; term->selection.kind = SELECTION_WORD_WISE; selection_update(term, end.col, end.row); break; } else { term->selection.kind = SELECTION_LINE_WISE; /* FALLTHROUGH */ } } case SELECTION_LINE_WISE: { struct coord start = {0, row}, end = {term->cols - 1, row}; selection_find_line_boundary_left(term, &start); selection_find_line_boundary_right(term, &end); term->selection.coords.start = (struct coord){ start.col, term->grid->view + start.row}; term->selection.pivot.start = term->selection.coords.start; term->selection.pivot.end = (struct coord){end.col, term->grid->view + end.row}; selection_update(term, end.col, end.row); break; } case SELECTION_NONE: BUG("Invalid selection kind"); break; } } static pixman_region32_t pixman_region_for_coords_normal(const struct terminal *term, const struct coord *start, const struct coord *end) { pixman_region32_t region; pixman_region32_init(®ion); const int rel_start_row = grid_row_abs_to_sb(term->grid, term->rows, start->row); const int rel_end_row = grid_row_abs_to_sb(term->grid, term->rows, end->row); if (rel_start_row < rel_end_row) { /* First partial row (start ->)*/ pixman_region32_union_rect( ®ion, ®ion, start->col, rel_start_row, term->cols - start->col, 1); /* Full rows between start and end */ if (rel_start_row + 1 < rel_end_row) { pixman_region32_union_rect( ®ion, ®ion, 0, rel_start_row + 1, term->cols, rel_end_row - rel_start_row - 1); } /* Last partial row (-> end) */ pixman_region32_union_rect( ®ion, ®ion, 0, rel_end_row, end->col + 1, 1); } else if (rel_start_row > rel_end_row) { /* First partial row (end ->) */ pixman_region32_union_rect( ®ion, ®ion, end->col, rel_end_row, term->cols - end->col, 1); /* Full rows between end and start */ if (rel_end_row + 1 < rel_start_row) { pixman_region32_union_rect( ®ion, ®ion, 0, rel_end_row + 1, term->cols, rel_start_row - rel_end_row - 1); } /* Last partial row (-> start) */ pixman_region32_union_rect( ®ion, ®ion, 0, rel_start_row, start->col + 1, 1); } else { const int start_col = min(start->col, end->col); const int end_col = max(start->col, end->col); pixman_region32_union_rect( ®ion, ®ion, start_col, rel_start_row, end_col + 1 - start_col, 1); } return region; } static pixman_region32_t pixman_region_for_coords_block(const struct terminal *term, const struct coord *start, const struct coord *end) { pixman_region32_t region; pixman_region32_init(®ion); const int rel_start_row = grid_row_abs_to_sb(term->grid, term->rows, start->row); const int rel_end_row = grid_row_abs_to_sb(term->grid, term->rows, end->row); pixman_region32_union_rect( ®ion, ®ion, min(start->col, end->col), min(rel_start_row, rel_end_row), abs(start->col - end->col) + 1, abs(rel_start_row - rel_end_row) + 1); return region; } /* Returns a pixman region representing the selection between 'start' * and 'end' (given the current selection kind), in *scrollback * relative coordinates* */ static pixman_region32_t pixman_region_for_coords(const struct terminal *term, const struct coord *start, const struct coord *end) { switch (term->selection.kind) { default: return pixman_region_for_coords_normal(term, start, end); case SELECTION_BLOCK: return pixman_region_for_coords_block(term, start, end); } } enum mark_selection_variant { MARK_SELECTION_MARK_AND_DIRTY, MARK_SELECTION_UNMARK_AND_DIRTY, MARK_SELECTION_MARK_FOR_RENDER, }; static void mark_selected_region(struct terminal *term, pixman_box32_t *boxes, size_t count, enum mark_selection_variant mark_variant) { const bool selected = mark_variant == MARK_SELECTION_MARK_AND_DIRTY || mark_variant == MARK_SELECTION_MARK_FOR_RENDER; const bool dirty_cells = mark_variant == MARK_SELECTION_MARK_AND_DIRTY || mark_variant == MARK_SELECTION_UNMARK_AND_DIRTY; const bool highlight_empty = mark_variant != MARK_SELECTION_MARK_FOR_RENDER || term->selection.kind == SELECTION_BLOCK; for (size_t i = 0; i < count; i++) { const pixman_box32_t *box = &boxes[i]; LOG_DBG("%s selection in region: %dx%d - %dx%d", selected ? "marking" : "unmarking", box->x1, box->y1, box->x2, box->y2); int abs_row_start = grid_row_sb_to_abs( term->grid, term->rows, box->y1); for (int r = abs_row_start, rel_r = box->y1; rel_r < box->y2; r = (r + 1) & (term->grid->num_rows - 1), rel_r++) { struct row *row = term->grid->rows[r]; xassert(row != NULL); if (dirty_cells) row->dirty = true; for (int c = box->x1, empty_count = 0; c < box->x2; c++) { struct cell *cell = &row->cells[c]; if (cell->wc == 0 && !highlight_empty) { /* * We used to highlight empty cells *if* they were * followed by non-empty cell(s), since this * corresponds to what gets extracted when the * selection is copied (that is, empty cells * "between" non-empty cells are converted to * spaces). * * However, they way we handle selection updates * (diffing the "old" selection area against the * "new" one, using pixman regions), means we * can't correctly update the state of empty * cells. The result is "random" empty cells being * rendered as selected when they shouldn't. * * "Fix" by *never* highlighting selected empty * cells (they still get converted to spaces when * copied, if followed by non-empty cells). */ empty_count++; /* * When the selection is *modified*, empty cells * are treated just like non-empty cells; they are * marked as selected, and dirtied. * * This is due to how the algorithm for updating * the selection works; it uses regions to * calculate the difference between the "old" and * the "new" selection. This makes it impossible * to tell if an empty cell is a *trailing* empty * cell (that should not be highlighted), or an * empty cells between non-empty cells (that * *should* be highlighted). * * Then, when a frame is rendered, we loop the * *visibible* cells that belong to the * selection. At this point, we *can* tell if an * empty cell is trailing or not. * * So, what we need to do is check if a * 'selected', and empty cell has been marked as * selected, temporarily unmark (forcing it dirty, * to ensure it gets re-rendered). If it is *not* * a trailing empty cell, it will get re-tagged as * selected in the for-loop below. */ cell->attrs.clean = false; cell->attrs.selected = false; row->dirty = true; continue; } for (int j = 0; j < empty_count + 1; j++) { xassert(c - j >= 0); struct cell *cell = &row->cells[c - j]; if (dirty_cells) { cell->attrs.clean = false; row->dirty = true; } cell->attrs.selected = selected; } empty_count = 0; } } } } static void selection_modify(struct terminal *term, struct coord start, struct coord end) { xassert(term->selection.coords.start.row != -1); xassert(start.row != -1 && start.col != -1); xassert(end.row != -1 && end.col != -1); pixman_region32_t previous_selection; if (term->selection.coords.end.row >= 0) { previous_selection = pixman_region_for_coords( term, &term->selection.coords.start, &term->selection.coords.end); } else pixman_region32_init(&previous_selection); pixman_region32_t current_selection = pixman_region_for_coords( term, &start, &end); pixman_region32_t no_longer_selected; pixman_region32_init(&no_longer_selected); pixman_region32_subtract( &no_longer_selected, &previous_selection, ¤t_selection); pixman_region32_t newly_selected; pixman_region32_init(&newly_selected); pixman_region32_subtract( &newly_selected, ¤t_selection, &previous_selection); /* Clear selection in cells no longer selected */ int n_rects = -1; pixman_box32_t *boxes = NULL; boxes = pixman_region32_rectangles(&no_longer_selected, &n_rects); mark_selected_region(term, boxes, n_rects, MARK_SELECTION_UNMARK_AND_DIRTY); boxes = pixman_region32_rectangles(&newly_selected, &n_rects); mark_selected_region(term, boxes, n_rects, MARK_SELECTION_MARK_AND_DIRTY); pixman_region32_fini(&newly_selected); pixman_region32_fini(&no_longer_selected); pixman_region32_fini(¤t_selection); pixman_region32_fini(&previous_selection); term->selection.coords.start = start; term->selection.coords.end = end; render_refresh(term); } static void set_pivot_point_for_block_and_char_wise(struct terminal *term, struct coord start, enum selection_direction new_direction) { struct coord *pivot_start = &term->selection.pivot.start; struct coord *pivot_end = &term->selection.pivot.end; *pivot_start = start; /* First, make sure 'start' isn't in the middle of a * multi-column character */ while (true) { const struct row *row = term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)]; const struct cell *cell = &row->cells[pivot_start->col]; if (cell->wc < CELL_SPACER) break; /* Multi-column chars don't cross rows */ xassert(pivot_start->col > 0); if (pivot_start->col == 0) break; pivot_start->col--; } /* * Setup pivot end to be one character *before* start * Which one we move, the end or start point, depends * on the initial selection direction. */ *pivot_end = *pivot_start; if (new_direction == SELECTION_RIGHT) { bool keep_going = true; while (keep_going) { const struct row *row = term->grid->rows[pivot_end->row & (term->grid->num_rows - 1)]; const char32_t wc = row->cells[pivot_end->col].wc; keep_going = wc >= CELL_SPACER; if (pivot_end->col == 0) { if (pivot_end->row - term->grid->view <= 0) break; pivot_end->col = term->cols - 1; pivot_end->row--; } else pivot_end->col--; } } else { bool keep_going = true; while (keep_going) { const struct row *row = term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)]; const char32_t wc = pivot_start->col < term->cols - 1 ? row->cells[pivot_start->col + 1].wc : 0; keep_going = wc >= CELL_SPACER; if (pivot_start->col >= term->cols - 1) { if (pivot_start->row - term->grid->view >= term->rows - 1) break; pivot_start->col = 0; pivot_start->row++; } else pivot_start->col++; } } xassert(term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)]-> cells[pivot_start->col].wc <= CELL_SPACER); xassert(term->grid->rows[pivot_end->row & (term->grid->num_rows - 1)]-> cells[pivot_end->col].wc <= CELL_SPACER + 1); } void selection_update(struct terminal *term, int col, int row) { if (term->selection.coords.start.row < 0) return; if (!term->selection.ongoing) return; xassert(term->grid->view + row != -1); struct coord new_start = term->selection.coords.start; struct coord new_end = {col, term->grid->view + row}; LOG_DBG("selection updated: start = %d,%d, end = %d,%d -> %d, %d", term->selection.coords.start.row, term->selection.coords.start.col, term->selection.coords.end.row, term->selection.coords.end.col, new_end.row, new_end.col); /* Adjust start point if the selection has changed 'direction' */ if (!(new_end.row == new_start.row && new_end.col == new_start.col)) { enum selection_direction new_direction = term->selection.direction; struct coord *pivot_start = &term->selection.pivot.start; struct coord *pivot_end = &term->selection.pivot.end; if (term->selection.kind == SELECTION_BLOCK) { if (new_end.col > pivot_start->col) new_direction = SELECTION_RIGHT; else new_direction = SELECTION_LEFT; if (term->selection.direction == SELECTION_UNDIR) set_pivot_point_for_block_and_char_wise(term, *pivot_start, new_direction); if (new_direction == SELECTION_LEFT) new_start = *pivot_end; else new_start = *pivot_start; term->selection.direction = new_direction; } else { if (new_end.row < pivot_start->row || (new_end.row == pivot_start->row && new_end.col < pivot_start->col)) { /* New end point is before the start point */ new_direction = SELECTION_LEFT; } else { /* The new end point is after the start point */ new_direction = SELECTION_RIGHT; } if (term->selection.direction != new_direction) { if (term->selection.direction == SELECTION_UNDIR && pivot_end->row < 0) { set_pivot_point_for_block_and_char_wise( term, *pivot_start, new_direction); } if (new_direction == SELECTION_LEFT) { xassert(pivot_end->row >= 0); new_start = *pivot_end; } else new_start = *pivot_start; term->selection.direction = new_direction; } } } switch (term->selection.kind) { case SELECTION_CHAR_WISE: case SELECTION_BLOCK: break; case SELECTION_WORD_WISE: switch (term->selection.direction) { case SELECTION_LEFT: new_end = (struct coord){col, term->grid->view + row}; selection_find_word_boundary_left( term, &new_end, term->selection.spaces_only); break; case SELECTION_RIGHT: new_end = (struct coord){col, term->grid->view + row}; selection_find_word_boundary_right( term, &new_end, term->selection.spaces_only, true); break; case SELECTION_UNDIR: break; } break; case SELECTION_QUOTE_WISE: BUG("quote-wise selection should always be transformed to either word-wise or line-wise"); break; case SELECTION_LINE_WISE: switch (term->selection.direction) { case SELECTION_LEFT: { struct coord end = {0, row}; selection_find_line_boundary_left(term, &end); new_end = (struct coord){end.col, term->grid->view + end.row}; break; } case SELECTION_RIGHT: { struct coord end = {col, row}; selection_find_line_boundary_right(term, &end); new_end = (struct coord){end.col, term->grid->view + end.row}; break; } case SELECTION_UNDIR: break; } break; case SELECTION_NONE: BUG("Invalid selection kind"); break; } size_t start_row_idx = new_start.row & (term->grid->num_rows - 1); size_t end_row_idx = new_end.row & (term->grid->num_rows - 1); const struct row *row_start = term->grid->rows[start_row_idx]; const struct row *row_end = term->grid->rows[end_row_idx]; /* If an end point is in the middle of a multi-column character, * expand the selection to cover the entire character */ if (new_start.row < new_end.row || (new_start.row == new_end.row && new_start.col <= new_end.col)) { while (new_start.col >= 1 && row_start->cells[new_start.col].wc >= CELL_SPACER) new_start.col--; while (new_end.col < term->cols - 1 && row_end->cells[new_end.col + 1].wc >= CELL_SPACER) new_end.col++; } else { while (new_end.col >= 1 && row_end->cells[new_end.col].wc >= CELL_SPACER) new_end.col--; while (new_start.col < term->cols - 1 && row_start->cells[new_start.col + 1].wc >= CELL_SPACER) new_start.col++; } selection_modify(term, new_start, new_end); } void selection_dirty_cells(struct terminal *term) { if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0) return; pixman_region32_t selection = pixman_region_for_coords( term, &term->selection.coords.start, &term->selection.coords.end); pixman_region32_t view = pixman_region_for_coords( term, &(struct coord){0, term->grid->view}, &(struct coord){term->cols - 1, term->grid->view + term->rows - 1}); pixman_region32_t visible_and_selected; pixman_region32_init(&visible_and_selected); pixman_region32_intersect(&visible_and_selected, &selection, &view); int n_rects = -1; pixman_box32_t *boxes = pixman_region32_rectangles(&visible_and_selected, &n_rects); mark_selected_region(term, boxes, n_rects, MARK_SELECTION_MARK_FOR_RENDER); pixman_region32_fini(&visible_and_selected); pixman_region32_fini(&view); pixman_region32_fini(&selection); } static void selection_extend_normal(struct terminal *term, int col, int row, enum selection_kind new_kind) { const struct coord *start = &term->selection.coords.start; const struct coord *end = &term->selection.coords.end; const int rel_row = grid_row_abs_to_sb(term->grid, term->rows, row); int rel_start_row = grid_row_abs_to_sb(term->grid, term->rows, start->row); int rel_end_row = grid_row_abs_to_sb(term->grid, term->rows, end->row); if (rel_start_row > rel_end_row || (rel_start_row == rel_end_row && start->col > end->col)) { const struct coord *tmp = start; start = end; end = tmp; int tmp_row = rel_start_row; rel_start_row = rel_end_row; rel_end_row = tmp_row; } struct coord new_start, new_end; enum selection_direction direction; if (rel_row < rel_start_row || (rel_row == rel_start_row && col < start->col)) { /* Extend selection to start *before* current start */ new_start = *end; new_end = (struct coord){col, row}; direction = SELECTION_LEFT; } else if (rel_row > rel_end_row || (rel_row == rel_end_row && col > end->col)) { /* Extend selection to end *after* current end */ new_start = *start; new_end = (struct coord){col, row}; direction = SELECTION_RIGHT; } else { /* Shrink selection from start or end, depending on which one is closest */ const int linear = rel_row * term->cols + col; if (abs(linear - (rel_start_row * term->cols + start->col)) < abs(linear - (rel_end_row * term->cols + end->col))) { /* Move start point */ new_start = *end; new_end = (struct coord){col, row}; direction = SELECTION_LEFT; } else { /* Move end point */ new_start = *start; new_end = (struct coord){col, row}; direction = SELECTION_RIGHT; } } const bool spaces_only = term->selection.spaces_only; switch (term->selection.kind) { case SELECTION_CHAR_WISE: xassert(new_kind == SELECTION_CHAR_WISE); set_pivot_point_for_block_and_char_wise(term, new_start, direction); break; case SELECTION_WORD_WISE: { xassert(new_kind == SELECTION_CHAR_WISE || new_kind == SELECTION_WORD_WISE); struct coord pivot_start = {new_start.col, new_start.row}; struct coord pivot_end = pivot_start; selection_find_word_boundary_left(term, &pivot_start, spaces_only); selection_find_word_boundary_right(term, &pivot_end, spaces_only, true); term->selection.pivot.start = pivot_start; term->selection.pivot.end = pivot_end; break; } case SELECTION_QUOTE_WISE: { BUG("quote-wise selection should always be transformed to either word-wise or line-wise"); break; } case SELECTION_LINE_WISE: { xassert(new_kind == SELECTION_CHAR_WISE || new_kind == SELECTION_LINE_WISE); struct coord pivot_start = {new_start.col, new_start.row - term->grid->view}; struct coord pivot_end = pivot_start; selection_find_line_boundary_left(term, &pivot_start); selection_find_line_boundary_right(term, &pivot_end); term->selection.pivot.start = (struct coord){pivot_start.col, term->grid->view + pivot_start.row}; term->selection.pivot.end = (struct coord){pivot_end.col, term->grid->view + pivot_end.row}; break; } case SELECTION_BLOCK: case SELECTION_NONE: BUG("Invalid selection kind in this context"); break; } term->selection.kind = new_kind; term->selection.direction = direction; selection_modify(term, new_start, new_end); } static void selection_extend_block(struct terminal *term, int col, int row) { const struct coord *start = &term->selection.coords.start; const struct coord *end = &term->selection.coords.end; const int rel_start_row = grid_row_abs_to_sb(term->grid, term->rows, start->row); const int rel_end_row = grid_row_abs_to_sb(term->grid, term->rows, end->row); struct coord top_left = { .row = rel_start_row < rel_end_row ? start->row : end->row, .col = min(start->col, end->col), }; struct coord top_right = { .row = top_left.row, .col = max(start->col, end->col), }; struct coord bottom_left = { .row = rel_start_row > rel_end_row ? start->row : end->row, .col = min(start->col, end->col), }; struct coord bottom_right = { .row = bottom_left.row, .col = max(start->col, end->col), }; const int rel_row = grid_row_abs_to_sb(term->grid, term->rows, row); const int rel_top_row = grid_row_abs_to_sb(term->grid, term->rows, top_left.row); const int rel_bottom_row = grid_row_abs_to_sb(term->grid, term->rows, bottom_left.row); struct coord new_start; struct coord new_end; enum selection_direction direction = SELECTION_UNDIR; if (rel_row <= rel_top_row || abs(rel_row - rel_top_row) < abs(rel_row - rel_bottom_row)) { /* Move one of the top corners */ if (abs(col - top_left.col) < abs(col - top_right.col)) { new_start = bottom_right; new_end = (struct coord){col, row}; } else { new_start = bottom_left; new_end = (struct coord){col, row}; } } else { /* Move one of the bottom corners */ if (abs(col - bottom_left.col) < abs(col - bottom_right.col)) { new_start = top_right; new_end = (struct coord){col, row}; } else { new_start = top_left; new_end = (struct coord){col, row}; } } direction = col > new_start.col ? SELECTION_RIGHT : SELECTION_LEFT; set_pivot_point_for_block_and_char_wise(term, new_start, direction); term->selection.direction = direction; selection_modify(term, new_start, new_end); } void selection_extend(struct seat *seat, struct terminal *term, int col, int row, enum selection_kind new_kind) { if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0) { /* No existing selection */ return; } if (term->selection.kind == SELECTION_BLOCK && new_kind != SELECTION_BLOCK) return; term->selection.ongoing = true; row += term->grid->view; if ((row == term->selection.coords.start.row && col == term->selection.coords.start.col) || (row == term->selection.coords.end.row && col == term->selection.coords.end.col)) { /* Extension point *is* one of the current end points */ return; } switch (term->selection.kind) { case SELECTION_NONE: BUG("Invalid selection kind"); return; case SELECTION_CHAR_WISE: case SELECTION_WORD_WISE: case SELECTION_QUOTE_WISE: case SELECTION_LINE_WISE: selection_extend_normal(term, col, row, new_kind); break; case SELECTION_BLOCK: selection_extend_block(term, col, row); break; } } //static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener; void selection_finalize(struct seat *seat, struct terminal *term, uint32_t serial) { if (!term->selection.ongoing) return; LOG_DBG("selection finalize"); selection_stop_scroll_timer(term); term->selection.ongoing = false; if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0) return; xassert(term->selection.coords.start.row != -1); xassert(term->selection.coords.end.row != -1); term->selection.coords.start.row &= (term->grid->num_rows - 1); term->selection.coords.end.row &= (term->grid->num_rows - 1); switch (term->conf->selection_target) { case SELECTION_TARGET_NONE: break; case SELECTION_TARGET_PRIMARY: selection_to_primary(seat, term, serial); break; case SELECTION_TARGET_CLIPBOARD: selection_to_clipboard(seat, term, serial); break; case SELECTION_TARGET_BOTH: selection_to_primary(seat, term, serial); selection_to_clipboard(seat, term, serial); break; } } static bool unmark_selected(struct terminal *term, struct row *row, struct cell *cell, int row_no, int col, void *data) { if (!cell->attrs.selected) return true; row->dirty = true; cell->attrs.selected = false; cell->attrs.clean = false; return true; } void selection_cancel(struct terminal *term) { LOG_DBG("selection cancelled: start = %d,%d end = %d,%d", term->selection.coords.start.row, term->selection.coords.start.col, term->selection.coords.end.row, term->selection.coords.end.col); selection_stop_scroll_timer(term); if (term->selection.coords.start.row >= 0 && term->selection.coords.end.row >= 0) { foreach_selected( term, term->selection.coords.start, term->selection.coords.end, &unmark_selected, NULL); render_refresh(term); } term->selection.kind = SELECTION_NONE; term->selection.coords.start = (struct coord){-1, -1}; term->selection.coords.end = (struct coord){-1, -1}; term->selection.pivot.start = (struct coord){-1, -1}; term->selection.pivot.end = (struct coord){-1, -1}; term->selection.direction = SELECTION_UNDIR; term->selection.ongoing = false; search_selection_cancelled(term); } bool selection_clipboard_has_data(const struct seat *seat) { return seat->clipboard.data_offer != NULL; } bool selection_primary_has_data(const struct seat *seat) { return seat->primary.data_offer != NULL; } void selection_clipboard_unset(struct seat *seat) { struct wl_clipboard *clipboard = &seat->clipboard; if (clipboard->data_source == NULL) return; /* Kill previous data source */ xassert(clipboard->serial != 0); wl_data_device_set_selection(seat->data_device, NULL, clipboard->serial); wl_data_source_destroy(clipboard->data_source); clipboard->data_source = NULL; clipboard->serial = 0; free(clipboard->text); clipboard->text = NULL; } void selection_primary_unset(struct seat *seat) { struct wl_primary *primary = &seat->primary; if (primary->data_source == NULL) return; xassert(primary->serial != 0); zwp_primary_selection_device_v1_set_selection( seat->primary_selection_device, NULL, primary->serial); zwp_primary_selection_source_v1_destroy(primary->data_source); primary->data_source = NULL; primary->serial = 0; free(primary->text); primary->text = NULL; } static bool fdm_scroll_timer(struct fdm *fdm, int fd, int events, void *data) { if (events & EPOLLHUP) return false; struct terminal *term = data; uint64_t expiration_count; ssize_t ret = read( term->selection.auto_scroll.fd, &expiration_count, sizeof(expiration_count)); if (ret < 0) { if (errno == EAGAIN) return true; LOG_ERRNO("failed to read selection scroll timer"); return false; } switch (term->selection.auto_scroll.direction) { case SELECTION_SCROLL_NOT: return true; case SELECTION_SCROLL_UP: cmd_scrollback_up(term, expiration_count); selection_update(term, term->selection.auto_scroll.col, 0); break; case SELECTION_SCROLL_DOWN: cmd_scrollback_down(term, expiration_count); selection_update(term, term->selection.auto_scroll.col, term->rows - 1); break; } return true; } void selection_start_scroll_timer(struct terminal *term, int interval_ns, enum selection_scroll_direction direction, int col) { xassert(direction != SELECTION_SCROLL_NOT); if (!term->selection.ongoing) return; if (term->selection.auto_scroll.fd < 0) { int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (fd < 0) { LOG_ERRNO("failed to create selection scroll timer"); goto err; } if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_scroll_timer, term)) { close(fd); return; } term->selection.auto_scroll.fd = fd; } struct itimerspec timer; if (timerfd_gettime(term->selection.auto_scroll.fd, &timer) < 0) { LOG_ERRNO("failed to get current selection scroll timer value"); goto err; } if (timer.it_value.tv_sec == 0 && timer.it_value.tv_nsec == 0) timer.it_value.tv_nsec = 1; timer.it_interval.tv_sec = interval_ns / 1000000000; timer.it_interval.tv_nsec = interval_ns % 1000000000; if (timerfd_settime(term->selection.auto_scroll.fd, 0, &timer, NULL) < 0) { LOG_ERRNO("failed to set new selection scroll timer value"); goto err; } term->selection.auto_scroll.direction = direction; term->selection.auto_scroll.col = col; return; err: selection_stop_scroll_timer(term); return; } void selection_stop_scroll_timer(struct terminal *term) { if (term->selection.auto_scroll.fd < 0) { xassert(term->selection.auto_scroll.direction == SELECTION_SCROLL_NOT); return; } fdm_del(term->fdm, term->selection.auto_scroll.fd); term->selection.auto_scroll.fd = -1; term->selection.auto_scroll.direction = SELECTION_SCROLL_NOT; } static void target(void *data, struct wl_data_source *wl_data_source, const char *mime_type) { LOG_DBG("TARGET: mime-type=%s", mime_type); } struct clipboard_send { char *data; size_t len; size_t idx; }; static bool fdm_send(struct fdm *fdm, int fd, int events, void *data) { struct clipboard_send *ctx = data; if (events & EPOLLHUP) goto done; switch (async_write(fd, ctx->data, ctx->len, &ctx->idx)) { case ASYNC_WRITE_REMAIN: return true; case ASYNC_WRITE_DONE: break; case ASYNC_WRITE_ERR: LOG_ERRNO( "failed to asynchronously write %zu of selection data to FD=%d", ctx->len - ctx->idx, fd); break; } done: fdm_del(fdm, fd); free(ctx->data); free(ctx); return true; } static void send_clipboard_or_primary(struct seat *seat, int fd, const char *selection, const char *source_name) { /* Make it NONBLOCK:ing right away - we don't want to block if the * initial attempt to send the data synchronously fails */ int flags; if ((flags = fcntl(fd, F_GETFL)) < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) { LOG_ERRNO("failed to set O_NONBLOCK"); return; } size_t len = selection != NULL ? strlen(selection) : 0; size_t async_idx = 0; switch (async_write(fd, selection, len, &async_idx)) { case ASYNC_WRITE_REMAIN: { struct clipboard_send *ctx = xmalloc(sizeof(*ctx)); *ctx = (struct clipboard_send) { .data = xstrdup(&selection[async_idx]), .len = len - async_idx, .idx = 0, }; if (fdm_add(seat->wayl->fdm, fd, EPOLLOUT, &fdm_send, ctx)) return; free(ctx->data); free(ctx); break; } case ASYNC_WRITE_DONE: break; case ASYNC_WRITE_ERR: LOG_ERRNO("failed write %zu bytes of %s selection data to FD=%d", len, source_name, fd); break; } close(fd); } static void send(void *data, struct wl_data_source *wl_data_source, const char *mime_type, int32_t fd) { struct seat *seat = data; const struct wl_clipboard *clipboard = &seat->clipboard; send_clipboard_or_primary(seat, fd, clipboard->text, "clipboard"); } static void cancelled(void *data, struct wl_data_source *wl_data_source) { struct seat *seat = data; struct wl_clipboard *clipboard = &seat->clipboard; xassert(clipboard->data_source == wl_data_source); wl_data_source_destroy(clipboard->data_source); clipboard->data_source = NULL; clipboard->serial = 0; free(clipboard->text); clipboard->text = NULL; } /* We don't support dragging *from* */ static void dnd_drop_performed(void *data, struct wl_data_source *wl_data_source) { //LOG_DBG("DnD drop performed"); } static void dnd_finished(void *data, struct wl_data_source *wl_data_source) { //LOG_DBG("DnD finished"); } static void action(void *data, struct wl_data_source *wl_data_source, uint32_t dnd_action) { //LOG_DBG("DnD action: %u", dnd_action); } static const struct wl_data_source_listener data_source_listener = { .target = &target, .send = &send, .cancelled = &cancelled, .dnd_drop_performed = &dnd_drop_performed, .dnd_finished = &dnd_finished, .action = &action, }; static void primary_send(void *data, struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1, const char *mime_type, int32_t fd) { struct seat *seat = data; const struct wl_primary *primary = &seat->primary; send_clipboard_or_primary(seat, fd, primary->text, "primary"); } static void primary_cancelled(void *data, struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1) { struct seat *seat = data; struct wl_primary *primary = &seat->primary; zwp_primary_selection_source_v1_destroy(primary->data_source); primary->data_source = NULL; primary->serial = 0; free(primary->text); primary->text = NULL; } static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener = { .send = &primary_send, .cancelled = &primary_cancelled, }; bool text_to_clipboard(struct seat *seat, struct terminal *term, char *text, uint32_t serial) { xassert(serial != 0); struct wl_clipboard *clipboard = &seat->clipboard; if (clipboard->data_source != NULL) { /* Kill previous data source */ xassert(clipboard->serial != 0); wl_data_device_set_selection(seat->data_device, NULL, clipboard->serial); wl_data_source_destroy(clipboard->data_source); free(clipboard->text); clipboard->data_source = NULL; clipboard->serial = 0; clipboard->text = NULL; } clipboard->data_source = wl_data_device_manager_create_data_source(term->wl->data_device_manager); if (clipboard->data_source == NULL) { LOG_ERR("failed to create clipboard data source"); return false; } clipboard->text = text; /* Configure source */ wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8]); wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_PLAIN]); wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_TEXT]);; wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_STRING]); wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8_STRING]); wl_data_source_add_listener(clipboard->data_source, &data_source_listener, seat); wl_data_device_set_selection(seat->data_device, clipboard->data_source, serial); /* Needed when sending the selection to other client */ clipboard->serial = serial; return true; } void selection_to_clipboard(struct seat *seat, struct terminal *term, uint32_t serial) { if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0) return; /* Get selection as a string */ char *text = selection_to_text(term); if (!text_to_clipboard(seat, term, text, serial)) free(text); } struct clipboard_receive { int read_fd; int timeout_fd; struct itimerspec timeout; bool bracketed; bool quote_paths; void (*decoder)(struct clipboard_receive *ctx, char *data, size_t size); void (*finish)(struct clipboard_receive *ctx); /* URI state */ bool add_space; struct { char *data; size_t sz; size_t idx; } buf; /* Callback data */ void (*cb)(char *data, size_t size, void *user); void (*done)(void *user); void *user; }; static void clipboard_receive_done(struct fdm *fdm, struct clipboard_receive *ctx) { fdm_del(fdm, ctx->timeout_fd); fdm_del(fdm, ctx->read_fd); ctx->done(ctx->user); free(ctx->buf.data); free(ctx); } static bool fdm_receive_timeout(struct fdm *fdm, int fd, int events, void *data) { struct clipboard_receive *ctx = data; if (events & EPOLLHUP) return false; xassert(events & EPOLLIN); uint64_t expire_count; ssize_t ret = read(fd, &expire_count, sizeof(expire_count)); if (ret < 0) { if (errno == EAGAIN) return true; LOG_ERRNO("failed to read clipboard timeout timer"); return false; } LOG_WARN("no data received from clipboard in %llu seconds, aborting", (unsigned long long)ctx->timeout.it_value.tv_sec); clipboard_receive_done(fdm, ctx); return true; } static void fdm_receive_decoder_plain(struct clipboard_receive *ctx, char *data, size_t size) { ctx->cb(data, size, ctx->user); } static void fdm_receive_finish_plain(struct clipboard_receive *ctx) { } static bool decode_one_uri(struct clipboard_receive *ctx, char *uri, size_t len) { LOG_DBG("URI: \"%.*s\"", (int)len, uri); if (len == 0) return false; char *scheme, *host, *path; if (!uri_parse(uri, len, &scheme, NULL, NULL, &host, NULL, &path, NULL, NULL)) { LOG_ERR("drag-and-drop: invalid URI: %.*s", (int)len, uri); return false; } if (ctx->add_space) ctx->cb(" ", 1, ctx->user); ctx->add_space = true; if (streq(scheme, "file") && hostname_is_localhost(host)) { if (ctx->quote_paths) ctx->cb("'", 1, ctx->user); ctx->cb(path, strlen(path), ctx->user); if (ctx->quote_paths) ctx->cb("'", 1, ctx->user); } else ctx->cb(uri, len, ctx->user); free(scheme); free(host); free(path); return true; } static void fdm_receive_decoder_uri(struct clipboard_receive *ctx, char *data, size_t size) { while (ctx->buf.idx + size > ctx->buf.sz) { size_t new_sz = ctx->buf.sz == 0 ? size : 2 * ctx->buf.sz; ctx->buf.data = xrealloc(ctx->buf.data, new_sz); ctx->buf.sz = new_sz; } memcpy(&ctx->buf.data[ctx->buf.idx], data, size); ctx->buf.idx += size; char *start = ctx->buf.data; char *end = NULL; while (true) { for (end = start; end < &ctx->buf.data[ctx->buf.idx]; end++) { if (*end == '\r' || *end == '\n') break; } if (end >= &ctx->buf.data[ctx->buf.idx]) break; decode_one_uri(ctx, start, end - start); start = end + 1; } const size_t ofs = start - ctx->buf.data; const size_t left = ctx->buf.idx - ofs; memmove(&ctx->buf.data[0], &ctx->buf.data[ofs], left); ctx->buf.idx = left; } static void fdm_receive_finish_uri(struct clipboard_receive *ctx) { LOG_DBG("finish: %.*s", (int)ctx->buf.idx, ctx->buf.data); decode_one_uri(ctx, ctx->buf.data, ctx->buf.idx); } static bool fdm_receive(struct fdm *fdm, int fd, int events, void *data) { struct clipboard_receive *ctx = data; if ((events & EPOLLHUP) && !(events & EPOLLIN)) goto done; /* Reset timeout timer */ if (timerfd_settime(ctx->timeout_fd, 0, &ctx->timeout, NULL) < 0) { LOG_ERRNO("failed to re-arm clipboard timeout timer"); return false; } /* Read until EOF */ while (true) { char text[256]; ssize_t count = read(fd, text, sizeof(text)); if (count == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) return true; LOG_ERRNO("failed to read clipboard data"); break; } if (count == 0) break; /* * Call cb while at same time replace: * - \r\n -> \r (non-bracketed paste) * - \n -> \r (non-bracketed paste) * - C0 -> (strip non-formatting C0 characters) * - \e -> (i.e. strip ESC) */ char *p = text; size_t left = count; #define skip_one() \ do { \ ctx->decoder(ctx, p, i); \ xassert(i + 1 <= left); \ p += i + 1; \ left -= i + 1; \ } while (0) again: for (size_t i = 0; i < left; i++) { switch (p[i]) { default: break; case '\n': if (!ctx->bracketed) p[i] = '\r'; break; case '\r': /* Convert \r\n -> \r */ if (!ctx->bracketed && i + 1 < left && p[i + 1] == '\n') { i++; skip_one(); goto again; } break; /* C0 non-formatting control characters (\b \t \n \r excluded) */ case '\x01': case '\x02': case '\x03': case '\x04': case '\x05': case '\x06': case '\x07': case '\x0e': case '\x0f': case '\x10': case '\x11': case '\x12': case '\x13': case '\x14': case '\x15': case '\x16': case '\x17': case '\x18': case '\x19': case '\x1a': case '\x1b': case '\x1c': case '\x1d': case '\x1e': case '\x1f': skip_one(); goto again; /* * In addition to stripping non-formatting C0 controls, * XTerm has an option, "disallowedPasteControls", that * defines C0 controls that will be replaced with spaces * when pasted. * * It's default value is BS,DEL,ENQ,EOT,NUL * * Instead of replacing them with spaces, we allow them in * bracketed paste mode, and strip them completely in * non-bracketed mode. * * Note some of the (default) XTerm controls are already * handled above. */ case '\b': case '\x7f': case '\x00': if (!ctx->bracketed) { skip_one(); goto again; } break; } } ctx->decoder(ctx, p, left); left = 0; } #undef skip_one done: ctx->finish(ctx); clipboard_receive_done(fdm, ctx); return true; } static void begin_receive_clipboard(struct terminal *term, int read_fd, enum data_offer_mime_type mime_type, void (*cb)(char *data, size_t size, void *user), void (*done)(void *user), void *user) { int timeout_fd = -1; struct clipboard_receive *ctx = NULL; int flags; if ((flags = fcntl(read_fd, F_GETFL)) < 0 || fcntl(read_fd, F_SETFL, flags | O_NONBLOCK) < 0) { LOG_ERRNO("failed to set O_NONBLOCK"); goto err; } timeout_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); if (timeout_fd < 0) { LOG_ERRNO("failed to create clipboard timeout timer FD"); goto err; } const struct itimerspec timeout = {.it_value = {.tv_sec = 2}}; if (timerfd_settime(timeout_fd, 0, &timeout, NULL) < 0) { LOG_ERRNO("failed to arm clipboard timeout timer"); goto err; } ctx = xmalloc(sizeof(*ctx)); *ctx = (struct clipboard_receive) { .read_fd = read_fd, .timeout_fd = timeout_fd, .timeout = timeout, .bracketed = term->bracketed_paste, .quote_paths = term->grid == &term->normal, .decoder = (mime_type == DATA_OFFER_MIME_URI_LIST ? &fdm_receive_decoder_uri : &fdm_receive_decoder_plain), .finish = (mime_type == DATA_OFFER_MIME_URI_LIST ? &fdm_receive_finish_uri : &fdm_receive_finish_plain), .cb = cb, .done = done, .user = user, }; if (!fdm_add(term->fdm, read_fd, EPOLLIN, &fdm_receive, ctx) || !fdm_add(term->fdm, timeout_fd, EPOLLIN, &fdm_receive_timeout, ctx)) { goto err; } return; err: free(ctx); fdm_del(term->fdm, timeout_fd); fdm_del(term->fdm, read_fd); done(user); } void text_from_clipboard(struct seat *seat, struct terminal *term, void (*cb)(char *data, size_t size, void *user), void (*done)(void *user), void *user) { struct wl_clipboard *clipboard = &seat->clipboard; if (clipboard->data_offer == NULL || clipboard->mime_type == DATA_OFFER_MIME_UNSET) { done(user); return; } /* Prepare a pipe the other client can write its selection to us */ int fds[2]; if (pipe2(fds, O_CLOEXEC) == -1) { LOG_ERRNO("failed to create pipe"); done(user); return; } LOG_DBG("receive from clipboard: mime-type=%s", mime_type_map[clipboard->mime_type]); int read_fd = fds[0]; int write_fd = fds[1]; /* Give write-end of pipe to other client */ wl_data_offer_receive( clipboard->data_offer, mime_type_map[clipboard->mime_type], write_fd); /* Don't keep our copy of the write-end open (or we'll never get EOF) */ close(write_fd); begin_receive_clipboard(term, read_fd, clipboard->mime_type, cb, done, user); } static void receive_offer(char *data, size_t size, void *user) { struct terminal *term = user; xassert(term->is_sending_paste_data); term_paste_data_to_slave(term, data, size); } static void receive_offer_done(void *user) { struct terminal *term = user; if (term->bracketed_paste) term_paste_data_to_slave(term, "\033[201~", 6); term->is_sending_paste_data = false; /* Make sure we send any queued up non-paste data */ if (tll_length(term->ptmx_buffers) > 0) fdm_event_add(term->fdm, term->ptmx, EPOLLOUT); } void selection_from_clipboard(struct seat *seat, struct terminal *term, uint32_t serial) { if (term->is_sending_paste_data) { /* We're already pasting... */ return; } struct wl_clipboard *clipboard = &seat->clipboard; if (clipboard->data_offer == NULL) return; term->is_sending_paste_data = true; if (term->bracketed_paste) term_paste_data_to_slave(term, "\033[200~", 6); text_from_clipboard(seat, term, &receive_offer, &receive_offer_done, term); } bool text_to_primary(struct seat *seat, struct terminal *term, char *text, uint32_t serial) { if (term->wl->primary_selection_device_manager == NULL) return false; xassert(serial != 0); struct wl_primary *primary = &seat->primary; /* TODO: somehow share code with the clipboard equivalent */ if (seat->primary.data_source != NULL) { /* Kill previous data source */ xassert(primary->serial != 0); zwp_primary_selection_device_v1_set_selection( seat->primary_selection_device, NULL, primary->serial); zwp_primary_selection_source_v1_destroy(primary->data_source); free(primary->text); primary->data_source = NULL; primary->serial = 0; primary->text = NULL; } primary->data_source = zwp_primary_selection_device_manager_v1_create_source( term->wl->primary_selection_device_manager); if (primary->data_source == NULL) { LOG_ERR("failed to create clipboard data source"); return false; } /* Get selection as a string */ primary->text = text; /* Configure source */ zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8]); zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_PLAIN]); zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_TEXT]); zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_STRING]); zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8_STRING]); zwp_primary_selection_source_v1_add_listener(primary->data_source, &primary_selection_source_listener, seat); zwp_primary_selection_device_v1_set_selection(seat->primary_selection_device, primary->data_source, serial); /* Needed when sending the selection to other client */ primary->serial = serial; return true; } void selection_to_primary(struct seat *seat, struct terminal *term, uint32_t serial) { if (term->wl->primary_selection_device_manager == NULL) return; /* Get selection as a string */ char *text = selection_to_text(term); if (!text_to_primary(seat, term, text, serial)) free(text); } void text_from_primary( struct seat *seat, struct terminal *term, void (*cb)(char *data, size_t size, void *user), void (*done)(void *user), void *user) { if (term->wl->primary_selection_device_manager == NULL) { done(user); return; } struct wl_primary *primary = &seat->primary; if (primary->data_offer == NULL || primary->mime_type == DATA_OFFER_MIME_UNSET) { done(user); return; } /* Prepare a pipe the other client can write its selection to us */ int fds[2]; if (pipe2(fds, O_CLOEXEC) == -1) { LOG_ERRNO("failed to create pipe"); done(user); return; } LOG_DBG("receive from primary: mime-type=%s", mime_type_map[primary->mime_type]); int read_fd = fds[0]; int write_fd = fds[1]; /* Give write-end of pipe to other client */ zwp_primary_selection_offer_v1_receive( primary->data_offer, mime_type_map[primary->mime_type], write_fd); /* Don't keep our copy of the write-end open (or we'll never get EOF) */ close(write_fd); begin_receive_clipboard(term, read_fd, primary->mime_type, cb, done, user); } void selection_from_primary(struct seat *seat, struct terminal *term) { if (term->wl->primary_selection_device_manager == NULL) return; if (term->is_sending_paste_data) { /* We're already pasting... */ return; } struct wl_primary *primary = &seat->primary; if (primary->data_offer == NULL) return; term->is_sending_paste_data = true; if (term->bracketed_paste) term_paste_data_to_slave(term, "\033[200~", 6); text_from_primary(seat, term, &receive_offer, &receive_offer_done, term); } static void select_mime_type_for_offer(const char *_mime_type, enum data_offer_mime_type *type) { enum data_offer_mime_type mime_type = DATA_OFFER_MIME_UNSET; /* Translate offered mime type to our mime type enum */ for (size_t i = 0; i < ALEN(mime_type_map); i++) { if (mime_type_map[i] == NULL) continue; if (streq(_mime_type, mime_type_map[i])) { mime_type = i; break; } } LOG_DBG("mime-type: %s -> %s (offered type was %s)", mime_type_map[*type], mime_type_map[mime_type], _mime_type); /* Mime-type transition; if the new mime-type is "better" than * previously offered types, use the new type */ switch (mime_type) { case DATA_OFFER_MIME_TEXT_PLAIN: case DATA_OFFER_MIME_TEXT_TEXT: case DATA_OFFER_MIME_TEXT_STRING: /* text/plain is our least preferred type. Only use if current * type is unset */ switch (*type) { case DATA_OFFER_MIME_UNSET: *type = mime_type; break; default: break; } break; case DATA_OFFER_MIME_TEXT_UTF8: case DATA_OFFER_MIME_TEXT_UTF8_STRING: /* text/plain;charset=utf-8 is preferred over text/plain */ switch (*type) { case DATA_OFFER_MIME_UNSET: case DATA_OFFER_MIME_TEXT_PLAIN: case DATA_OFFER_MIME_TEXT_TEXT: case DATA_OFFER_MIME_TEXT_STRING: *type = mime_type; break; default: break; } break; case DATA_OFFER_MIME_URI_LIST: /* text/uri-list is always used when offered */ *type = mime_type; break; case DATA_OFFER_MIME_UNSET: break; } } static void data_offer_reset(struct wl_clipboard *clipboard) { if (clipboard->data_offer != NULL) { wl_data_offer_destroy(clipboard->data_offer); clipboard->data_offer = NULL; } clipboard->window = NULL; clipboard->mime_type = DATA_OFFER_MIME_UNSET; } static void offer(void *data, struct wl_data_offer *wl_data_offer, const char *mime_type) { struct seat *seat = data; select_mime_type_for_offer(mime_type, &seat->clipboard.mime_type); } static void source_actions(void *data, struct wl_data_offer *wl_data_offer, uint32_t source_actions) { #if defined(_DEBUG) && LOG_ENABLE_DBG char actions_as_string[1024]; size_t idx = 0; actions_as_string[0] = '\0'; actions_as_string[sizeof(actions_as_string) - 1] = '\0'; for (size_t i = 0; i < 31; i++) { if (((source_actions >> i) & 1) == 0) continue; enum wl_data_device_manager_dnd_action action = 1 << i; const char *s = NULL; switch (action) { case WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE: s = NULL; break; case WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY: s = "copy"; break; case WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE: s = "move"; break; case WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK: s = "ask"; break; } if (s == NULL) continue; strncat(actions_as_string, s, sizeof(actions_as_string) - idx - 1); idx += strlen(s); strncat(actions_as_string, ", ", sizeof(actions_as_string) - idx - 1); idx += 2; } /* Strip trailing ", " */ if (strlen(actions_as_string) > 2) actions_as_string[strlen(actions_as_string) - 2] = '\0'; LOG_DBG("DnD actions: %s (0x%08x)", actions_as_string, source_actions); #endif } static void offer_action(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action) { #if defined(_DEBUG) && LOG_ENABLE_DBG const char *s = NULL; switch (dnd_action) { case WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE: s = ""; break; case WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY: s = "copy"; break; case WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE: s = "move"; break; case WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK: s = "ask"; break; } LOG_DBG("DnD offer action: %s (0x%08x)", s, dnd_action); #endif } static const struct wl_data_offer_listener data_offer_listener = { .offer = &offer, .source_actions = &source_actions, .action = &offer_action, }; static void data_offer(void *data, struct wl_data_device *wl_data_device, struct wl_data_offer *offer) { struct seat *seat = data; data_offer_reset(&seat->clipboard); seat->clipboard.data_offer = offer; wl_data_offer_add_listener(offer, &data_offer_listener, seat); } static void enter(void *data, struct wl_data_device *wl_data_device, uint32_t serial, struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y, struct wl_data_offer *offer) { struct seat *seat = data; struct wayland *wayl = seat->wayl; xassert(offer == seat->clipboard.data_offer); if (seat->clipboard.mime_type == DATA_OFFER_MIME_UNSET) goto reject_offer; /* Remember _which_ terminal the current DnD offer is targeting */ xassert(seat->clipboard.window == NULL); tll_foreach(wayl->terms, it) { if (term_surface_kind(it->item, surface) == TERM_SURF_GRID && !it->item->is_sending_paste_data) { wl_data_offer_accept( offer, serial, mime_type_map[seat->clipboard.mime_type]); wl_data_offer_set_actions( offer, WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY, WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY); seat->clipboard.window = it->item->window; return; } } reject_offer: /* Either terminal is already busy sending paste data, or mouse * pointer isn't over the grid */ seat->clipboard.window = NULL; wl_data_offer_accept(offer, serial, NULL); wl_data_offer_set_actions( offer, WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE, WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE); } static void leave(void *data, struct wl_data_device *wl_data_device) { struct seat *seat = data; seat->clipboard.window = NULL; } static void motion(void *data, struct wl_data_device *wl_data_device, uint32_t time, wl_fixed_t x, wl_fixed_t y) { } struct dnd_context { struct terminal *term; struct wl_data_offer *data_offer; }; static void receive_dnd(char *data, size_t size, void *user) { struct dnd_context *ctx = user; receive_offer(data, size, ctx->term); } static void receive_dnd_done(void *user) { struct dnd_context *ctx = user; wl_data_offer_finish(ctx->data_offer); wl_data_offer_destroy(ctx->data_offer); receive_offer_done(ctx->term); free(ctx); } static void drop(void *data, struct wl_data_device *wl_data_device) { struct seat *seat = data; xassert(seat->clipboard.window != NULL); struct terminal *term = seat->clipboard.window->term; struct wl_clipboard *clipboard = &seat->clipboard; if (clipboard->mime_type == DATA_OFFER_MIME_UNSET) { LOG_WARN("compositor called data_device::drop() " "even though we rejected the drag-and-drop"); return; } struct dnd_context *ctx = xmalloc(sizeof(*ctx)); *ctx = (struct dnd_context){ .term = term, .data_offer = clipboard->data_offer, }; /* Prepare a pipe the other client can write its selection to us */ int fds[2]; if (pipe2(fds, O_CLOEXEC) == -1) { LOG_ERRNO("failed to create pipe"); free(ctx); return; } int read_fd = fds[0]; int write_fd = fds[1]; LOG_DBG("DnD drop: mime-type=%s", mime_type_map[clipboard->mime_type]); /* Give write-end of pipe to other client */ wl_data_offer_receive( clipboard->data_offer, mime_type_map[clipboard->mime_type], write_fd); /* Don't keep our copy of the write-end open (or we'll never get EOF) */ close(write_fd); term->is_sending_paste_data = true; if (term->bracketed_paste) term_paste_data_to_slave(term, "\033[200~", 6); begin_receive_clipboard( term, read_fd, clipboard->mime_type, &receive_dnd, &receive_dnd_done, ctx); /* data offer is now "owned" by the receive context */ clipboard->data_offer = NULL; clipboard->mime_type = DATA_OFFER_MIME_UNSET; } static void selection(void *data, struct wl_data_device *wl_data_device, struct wl_data_offer *offer) { /* Selection offer from other client */ struct seat *seat = data; if (offer == NULL) data_offer_reset(&seat->clipboard); else xassert(offer == seat->clipboard.data_offer); } const struct wl_data_device_listener data_device_listener = { .data_offer = &data_offer, .enter = &enter, .leave = &leave, .motion = &motion, .drop = &drop, .selection = &selection, }; static void primary_offer(void *data, struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer, const char *mime_type) { LOG_DBG("primary offer: %s", mime_type); struct seat *seat = data; select_mime_type_for_offer(mime_type, &seat->primary.mime_type); } static const struct zwp_primary_selection_offer_v1_listener primary_selection_offer_listener = { .offer = &primary_offer, }; static void primary_offer_reset(struct wl_primary *primary) { if (primary->data_offer != NULL) { zwp_primary_selection_offer_v1_destroy(primary->data_offer); primary->data_offer = NULL; } primary->mime_type = DATA_OFFER_MIME_UNSET; } static void primary_data_offer(void *data, struct zwp_primary_selection_device_v1 *zwp_primary_selection_device, struct zwp_primary_selection_offer_v1 *offer) { struct seat *seat = data; primary_offer_reset(&seat->primary); seat->primary.data_offer = offer; zwp_primary_selection_offer_v1_add_listener( offer, &primary_selection_offer_listener, seat); } static void primary_selection(void *data, struct zwp_primary_selection_device_v1 *zwp_primary_selection_device, struct zwp_primary_selection_offer_v1 *offer) { /* Selection offer from other client, for primary */ struct seat *seat = data; if (offer == NULL) primary_offer_reset(&seat->primary); else xassert(seat->primary.data_offer == offer); } const struct zwp_primary_selection_device_v1_listener primary_selection_device_listener = { .data_offer = &primary_data_offer, .selection = &primary_selection, }; foot-1.21.0/selection.h000066400000000000000000000063671476600145200147210ustar00rootroot00000000000000#pragma once #include #include #include "terminal.h" extern const struct wl_data_device_listener data_device_listener; extern const struct zwp_primary_selection_device_v1_listener primary_selection_device_listener; void selection_start( struct terminal *term, int col, int row, enum selection_kind new_kind, bool spaces_only); void selection_update(struct terminal *term, int col, int row); void selection_finalize( struct seat *seat, struct terminal *term, uint32_t serial); void selection_dirty_cells(struct terminal *term); void selection_cancel(struct terminal *term); void selection_extend( struct seat *seat, struct terminal *term, int col, int row, enum selection_kind kind); bool selection_on_rows(const struct terminal *term, int start, int end); void selection_scroll_up(struct terminal *term, int rows); void selection_scroll_down(struct terminal *term, int rows); void selection_view_up(struct terminal *term, int new_view); void selection_view_down(struct terminal *term, int new_view); void selection_clipboard_unset(struct seat *seat); void selection_primary_unset(struct seat *seat); bool selection_clipboard_has_data(const struct seat *seat); bool selection_primary_has_data(const struct seat *seat); char *selection_to_text(const struct terminal *term); void selection_to_clipboard( struct seat *seat, struct terminal *term, uint32_t serial); void selection_from_clipboard( struct seat *seat, struct terminal *term, uint32_t serial); void selection_to_primary( struct seat *seat, struct terminal *term, uint32_t serial); void selection_from_primary(struct seat *seat, struct terminal *term); /* Copy text *to* primary/clipboard */ bool text_to_clipboard( struct seat *seat, struct terminal *term, char *text, uint32_t serial); bool text_to_primary( struct seat *seat, struct terminal *term, char *text, uint32_t serial); /* * Copy text *from* primary/clipboard * * Note that these are asynchronous; they *will* return * immediately. The 'cb' callback will be called 0..n times with * clipboard data. When done (or on error), the 'done' callback is * called. * * As such, keep this in mind: * - The 'user' context must not be stack allocated * - Don't expect clipboard data to have been received when these * functions return (it will *never* have been received at this * point). */ void text_from_clipboard( struct seat *seat, struct terminal *term, 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, void (*cb)(char *data, size_t size, void *user), void (*dont)(void *user), void *user); void selection_start_scroll_timer( struct terminal *term, int interval_ns, enum selection_scroll_direction direction, int col); void selection_stop_scroll_timer(struct terminal *term); void selection_find_word_boundary_left( const struct terminal *term, struct coord *pos, bool spaces_only); void selection_find_word_boundary_right( const struct terminal *term, struct coord *pos, bool spaces_only, bool stop_on_space_to_word_boundary); struct coord selection_get_start(const struct terminal *term); struct coord selection_get_end(const struct terminal *term); foot-1.21.0/server.c000066400000000000000000000400451476600145200142240ustar00rootroot00000000000000#include "server.h" #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "server" #define LOG_ENABLE_DBG 0 #include "log.h" #include "client-protocol.h" #include "terminal.h" #include "util.h" #include "wayland.h" #include "xmalloc.h" #define NON_ZERO_OPT (INT_MIN / 7) struct client; struct terminal_instance; struct server { const struct config *conf; struct fdm *fdm; struct reaper *reaper; struct wayland *wayl; int fd; const char *sock_path; tll(struct client *) clients; tll(struct terminal_instance *) terminals; }; struct client { struct server *server; int fd; struct { uint8_t *data; size_t left; size_t idx; } buffer; struct terminal_instance *instance; }; static void client_destroy(struct client *client); struct terminal_instance { struct terminal *terminal; struct server *server; struct client *client; struct config *conf; }; static void instance_destroy(struct terminal_instance *instance, int exit_code); static void client_destroy(struct client *client) { if (client == NULL) return; if (client->instance != NULL) { LOG_WARN("client FD=%d: terminal still alive", client->fd); client->instance->client = NULL; instance_destroy(client->instance, 1); } if (client->fd != -1) { LOG_DBG("client FD=%d: disconnected", client->fd); fdm_del(client->server->fdm, client->fd); } tll_foreach(client->server->clients, it) { if (it->item == client) { tll_remove(client->server->clients, it); break; } } free(client->buffer.data); free(client); } static void client_send_exit_code(struct client *client, int exit_code) { if (client->fd == -1) return; if (write(client->fd, &exit_code, sizeof(exit_code)) != sizeof(exit_code)) LOG_ERRNO("failed to write slave exit code to client"); } static void instance_destroy(struct terminal_instance *instance, int exit_code) { if (instance->terminal != NULL) term_destroy(instance->terminal); tll_foreach(instance->server->terminals, it) { if (it->item == instance) { tll_remove(instance->server->terminals, it); break; } } if (instance->client != NULL) { instance->client->instance = NULL; client_send_exit_code(instance->client, exit_code); client_destroy(instance->client); } /* TODO: clone server conf completely, so that we can just call * conf_destroy() here */ if (instance->conf != NULL) { config_free(instance->conf); free(instance->conf); } free(instance); } static void term_shutdown_handler(void *data, int exit_code) { struct terminal_instance *instance = data; instance->terminal = NULL; instance_destroy(instance, exit_code); } static bool fdm_client(struct fdm *fdm, int fd, int events, void *data) { struct client *client = data; struct server *server = client->server; char **argv = NULL; config_override_t overrides = tll_init(); char **envp = NULL; if (events & EPOLLHUP) goto shutdown; xassert(events & EPOLLIN); if (client->instance != NULL) { 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? */ } if (client->buffer.data == NULL) { /* * We haven't received any data yet - the first thing the * client sends is the total size of the initialization * data. */ uint32_t total_len; ssize_t count = recv(fd, &total_len, sizeof(total_len), 0); if (count < 0) { LOG_ERRNO("failed to read total length"); goto shutdown; } if (count != sizeof(total_len)) { LOG_ERR("client did not send setup packet size"); goto shutdown; } const uint32_t max_size = 128 * 1024; if (total_len > max_size) { LOG_ERR("client wants to send too large setup packet (%u > %u)", total_len, max_size); goto shutdown; } LOG_DBG("total len: %u", total_len); client->buffer.data = xmalloc(total_len + 1); client->buffer.left = total_len; client->buffer.idx = 0; /* Prevent our strlen() calls to run outside */ client->buffer.data[total_len] = '\0'; return true; /* Let FDM trigger again when we have more data */ } /* Keep filling our buffer of initialization data */ ssize_t count = recv( fd, &client->buffer.data[client->buffer.idx], client->buffer.left, 0); if (count < 0) { LOG_ERRNO("failed to read"); goto shutdown; } client->buffer.idx += count; client->buffer.left -= count; if (client->buffer.left > 0) { /* Not done yet */ return true; } if (tll_length(server->wayl->monitors) == 0) { LOG_ERR("no monitors available for new terminal"); client_send_exit_code(client, -26); goto shutdown; } /* All initialization data received - time to instantiate a terminal! */ xassert(client->instance == NULL); xassert(client->buffer.data != NULL); xassert(client->buffer.left == 0); /* * Parse the received buffer, verifying lengths etc */ #define CHECK_BUF(sz) do { \ if (p + (sz) > end) \ goto shutdown; \ } while (0) #define CHECK_BUF_AND_NULL(sz) do { \ CHECK_BUF(sz); \ if (sz == 0) \ goto shutdown; \ if (p[sz - 1] != '\0') \ goto shutdown; \ } while (0) uint8_t *p = client->buffer.data; const uint8_t *end = &client->buffer.data[client->buffer.idx]; struct client_data cdata; CHECK_BUF(sizeof(cdata)); memcpy(&cdata, p, sizeof(cdata)); p += sizeof(cdata); CHECK_BUF_AND_NULL(cdata.cwd_len); const char *cwd = (const char *)p; p += cdata.cwd_len; LOG_DBG("CWD = %.*s", cdata.cwd_len, cwd); /* XDGA token */ const char *token = NULL; if (cdata.xdga_token) { CHECK_BUF_AND_NULL(cdata.token_len); token = (const char *)p; p += cdata.token_len; LOG_DBG("XDGA = %.*s", cdata.token_len, token); } else { LOG_DBG("No XDGA token"); } /* Overrides */ for (uint16_t i = 0; i < cdata.override_count; i++) { struct client_string arg; CHECK_BUF(sizeof(arg)); memcpy(&arg, p, sizeof(arg)); p += sizeof(arg); CHECK_BUF_AND_NULL(arg.len); const char *str = (const char *)p; p += arg.len; tll_push_back(overrides, xstrdup(str)); } /* argv */ argv = xcalloc(cdata.argc + 1, sizeof(argv[0])); for (uint16_t i = 0; i < cdata.argc; i++) { struct client_string arg; CHECK_BUF(sizeof(arg)); memcpy(&arg, p, sizeof(arg)); p += sizeof(arg); CHECK_BUF_AND_NULL(arg.len); argv[i] = (char *)p; p += arg.len; LOG_DBG("argv[%hu] = %.*s", i, arg.len, argv[i]); } /* envp */ envp = cdata.env_count != 0 ? xcalloc(cdata.env_count + 1, sizeof(envp[0])) : NULL; for (uint16_t i = 0; i < cdata.env_count; i++) { struct client_string e; CHECK_BUF(sizeof(e)); memcpy(&e, p, sizeof(e)); p += sizeof(e); CHECK_BUF_AND_NULL(e.len); envp[i] = (char *)p; p += e.len; LOG_DBG("env[%hu] = %.*s", i, e.len, envp[i]); } #undef CHECK_BUF_AND_NULL #undef CHECK_BUF struct terminal_instance *instance = xmalloc(sizeof(struct terminal_instance)); const bool need_to_clone_conf = tll_length(overrides)> 0 || cdata.hold != server->conf->hold_at_exit; struct config *conf = NULL; if (need_to_clone_conf) { conf = config_clone(server->conf); if (cdata.hold != server->conf->hold_at_exit) conf->hold_at_exit = cdata.hold; config_override_apply(conf, &overrides, false); if (conf->tweak.font_monospace_warn && conf->fonts[0].count > 0) { check_if_font_is_monospaced( conf->fonts[0].arr[0].pattern, &conf->notifications); } } *instance = (struct terminal_instance) { .client = NULL, .server = server, .conf = conf, }; instance->terminal = term_init( conf != NULL ? conf : server->conf, server->fdm, server->reaper, server->wayl, "footclient", cwd, token, NULL, cdata.argc, argv, (const char *const *)envp, &term_shutdown_handler, instance); if (instance->terminal == NULL) { LOG_ERR("failed to instantiate new terminal"); client_send_exit_code(client, -26); instance_destroy(instance, -1); goto shutdown; } if (cdata.no_wait) { // the server owns the instance tll_push_back(server->terminals, instance); client_send_exit_code(client, 0); goto shutdown; } else { // the instance is attached to the client instance->client = client; client->instance = instance; free(argv); free(envp); tll_free_and_free(overrides, free); } return true; shutdown: LOG_DBG("client FD=%d: disconnected", client->fd); free(argv); free(envp); tll_free_and_free(overrides, free); fdm_del(fdm, fd); client->fd = -1; if (client->instance != NULL && !client->instance->terminal->shutdown.in_progress) { term_shutdown(client->instance->terminal); } else client_destroy(client); return true; } static bool fdm_server(struct fdm *fdm, int fd, int events, void *data) { if (events & EPOLLHUP) return false; struct server *server = data; struct sockaddr_un addr; socklen_t addr_size = sizeof(addr); int client_fd = accept4( server->fd, (struct sockaddr *)&addr, &addr_size, SOCK_CLOEXEC | SOCK_NONBLOCK); if (client_fd == -1) { LOG_ERRNO("failed to accept client connection"); return false; } struct client *client = xmalloc(sizeof(*client)); *client = (struct client) { .server = server, .fd = client_fd, }; if (!fdm_add(server->fdm, client_fd, EPOLLIN, &fdm_client, client)) { close(client_fd); free(client); return false; } LOG_DBG("client FD=%d: connected", client_fd); tll_push_back(server->clients, client); return true; } enum connect_status {CONNECT_ERR, CONNECT_FAIL, CONNECT_SUCCESS}; static enum connect_status try_connect(const char *sock_path) { enum connect_status ret = CONNECT_ERR; int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); if (fd == -1) { LOG_ERRNO("failed to create UNIX socket"); goto err; } struct sockaddr_un addr = {.sun_family = AF_UNIX}; strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1); switch (connect(fd, (struct sockaddr *)&addr, sizeof(addr))) { case 0: ret = CONNECT_SUCCESS; break; case -1: LOG_DBG("connect() failed: %s", strerror(errno)); ret = CONNECT_FAIL; break; } err: if (fd != -1) close(fd); return ret; } static bool prepare_socket(int fd) { int flags = fcntl(fd, F_GETFD); if (flags < 0) { LOG_ERRNO("failed to get file descriptors flag for passed socket"); return false; } if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) { LOG_ERRNO("failed to set FD_CLOEXEC for passed socket"); return false; } flags = fcntl(fd, F_GETFL); if (flags < 0) { LOG_ERRNO("failed to get file status flags for passed socket"); return false; } if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { LOG_ERRNO("failed to set non-blocking mode on passed socket"); return false; } int const socket_options[] = { SO_DOMAIN, SO_ACCEPTCONN, SO_TYPE }; int const socket_options_values[] = { AF_UNIX, NON_ZERO_OPT, SOCK_STREAM}; char const * const socket_options_names[] = { "SO_DOMAIN", "SO_ACCEPTCONN", "SO_TYPE" }; xassert(ALEN(socket_options) == ALEN(socket_options_values)); xassert(ALEN(socket_options) == ALEN(socket_options_names)); int socket_option = 0; socklen_t len; for (size_t i = 0; i < ALEN(socket_options) ; i++) { len = sizeof(socket_option); if (getsockopt(fd, SOL_SOCKET, socket_options[i], &socket_option, &len) == -1 || len != sizeof(socket_option)) { LOG_ERRNO("failed to read socket option from passed file descriptor"); return false; } if (socket_options_values[i] == NON_ZERO_OPT && socket_option) socket_option = NON_ZERO_OPT; if (socket_option != socket_options_values[i]) { LOG_ERR("wrong socket value for socket option '%s' on passed file descriptor", socket_options_names[i]); return false; } } return true; } struct server * server_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl) { int fd; struct server *server = NULL; const char *sock_path = conf->server_socket_path; char *end; errno = 0; fd = strtol(sock_path, &end, 10); if (*end == '\0' && *sock_path != '\0') { if (!prepare_socket(fd)) goto err; LOG_DBG("we've been started by socket activation, using passed socket"); sock_path = NULL; } else { LOG_DBG("no suitable pre-existing socket found, creating our own"); fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); if (fd == -1) { LOG_ERRNO("failed to create UNIX socket"); return NULL; } switch (try_connect(sock_path)) { case CONNECT_FAIL: break; case CONNECT_SUCCESS: LOG_ERR("%s is already accepting connections; is 'foot --server' already running", sock_path); /* FALLTHROUGH */ case CONNECT_ERR: goto err; } unlink(sock_path); struct sockaddr_un addr = {.sun_family = AF_UNIX}; strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1); if (bind(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { LOG_ERRNO("%s: failed to bind", addr.sun_path); goto err; } if (listen(fd, 0) < 0) { LOG_ERRNO("%s: failed to listen", addr.sun_path); goto err; } } server = malloc(sizeof(*server)); if (unlikely(server == NULL)) { LOG_ERRNO("malloc() failed"); goto err; } *server = (struct server) { .conf = conf, .fdm = fdm, .reaper = reaper, .wayl = wayl, .fd = fd, .sock_path = sock_path, .clients = tll_init(), .terminals = tll_init(), }; if (!fdm_add(fdm, fd, EPOLLIN, &fdm_server, server)) goto err; LOG_INFO("accepting connections on %s", sock_path != NULL ? sock_path : "socket provided through socket activation"); return server; err: free(server); if (fd != -1) close(fd); return NULL; } void server_destroy(struct server *server) { if (server == NULL) return; LOG_DBG("server destroy, %zu clients still alive", tll_length(server->clients)); tll_foreach(server->clients, it) { client_send_exit_code(it->item, -26); client_destroy(it->item); } tll_free(server->clients); tll_foreach(server->terminals, it) instance_destroy(it->item, 1); tll_free(server->terminals); fdm_del(server->fdm, server->fd); if (server->sock_path != NULL) unlink(server->sock_path); free(server); } foot-1.21.0/server.h000066400000000000000000000004501476600145200142250ustar00rootroot00000000000000#pragma once #include "fdm.h" #include "config.h" #include "reaper.h" #include "wayland.h" struct server; struct server *server_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl); void server_destroy(struct server *server); foot-1.21.0/shm-formats.h000066400000000000000000000121221476600145200151560ustar00rootroot00000000000000#pragma once #include #if defined(_DEBUG) static const struct shm_formats { uint32_t format; const char *description; } shm_formats[] = { {WL_SHM_FORMAT_ARGB8888, "ARGB8888"}, {WL_SHM_FORMAT_XRGB8888, "XRGB8888"}, {WL_SHM_FORMAT_C8, "C8"}, {WL_SHM_FORMAT_RGB332, "RGB332"}, {WL_SHM_FORMAT_BGR233, "BGR233"}, {WL_SHM_FORMAT_XRGB4444, "XRGB4444"}, {WL_SHM_FORMAT_XBGR4444, "XBGR4444"}, {WL_SHM_FORMAT_RGBX4444, "RGBX4444"}, {WL_SHM_FORMAT_BGRX4444, "BGRX4444"}, {WL_SHM_FORMAT_ARGB4444, "ARGB4444"}, {WL_SHM_FORMAT_ABGR4444, "ABGR4444"}, {WL_SHM_FORMAT_RGBA4444, "RGBA4444"}, {WL_SHM_FORMAT_BGRA4444, "BGRA4444"}, {WL_SHM_FORMAT_XRGB1555, "XRGB1555"}, {WL_SHM_FORMAT_XBGR1555, "XBGR1555"}, {WL_SHM_FORMAT_RGBX5551, "RGBX5551"}, {WL_SHM_FORMAT_BGRX5551, "BGRX5551"}, {WL_SHM_FORMAT_ARGB1555, "ARGB1555"}, {WL_SHM_FORMAT_ABGR1555, "ABGR1555"}, {WL_SHM_FORMAT_RGBA5551, "RGBA5551"}, {WL_SHM_FORMAT_BGRA5551, "BGRA5551"}, {WL_SHM_FORMAT_RGB565, "RGB565"}, {WL_SHM_FORMAT_BGR565, "BGR565"}, {WL_SHM_FORMAT_RGB888, "RGB888"}, {WL_SHM_FORMAT_BGR888, "BGR888"}, {WL_SHM_FORMAT_XBGR8888, "XBGR8888"}, {WL_SHM_FORMAT_RGBX8888, "RGBX8888"}, {WL_SHM_FORMAT_BGRX8888, "BGRX8888"}, {WL_SHM_FORMAT_ABGR8888, "ABGR8888"}, {WL_SHM_FORMAT_RGBA8888, "RGBA8888"}, {WL_SHM_FORMAT_BGRA8888, "BGRA8888"}, {WL_SHM_FORMAT_XRGB2101010, "XRGB2101010"}, {WL_SHM_FORMAT_XBGR2101010, "XBGR2101010"}, {WL_SHM_FORMAT_RGBX1010102, "RGBX1010102"}, {WL_SHM_FORMAT_BGRX1010102, "BGRX1010102"}, {WL_SHM_FORMAT_ARGB2101010, "ARGB2101010"}, {WL_SHM_FORMAT_ABGR2101010, "ABGR2101010"}, {WL_SHM_FORMAT_RGBA1010102, "RGBA1010102"}, {WL_SHM_FORMAT_BGRA1010102, "BGRA1010102"}, {WL_SHM_FORMAT_YUYV, "YUYV"}, {WL_SHM_FORMAT_YVYU, "YVYU"}, {WL_SHM_FORMAT_UYVY, "UYVY"}, {WL_SHM_FORMAT_VYUY, "VYUY"}, {WL_SHM_FORMAT_AYUV, "AYUV"}, {WL_SHM_FORMAT_NV12, "NV12"}, {WL_SHM_FORMAT_NV21, "NV21"}, {WL_SHM_FORMAT_NV16, "NV16"}, {WL_SHM_FORMAT_NV61, "NV61"}, {WL_SHM_FORMAT_YUV410, "YUV410"}, {WL_SHM_FORMAT_YVU410, "YVU410"}, {WL_SHM_FORMAT_YUV411, "YUV411"}, {WL_SHM_FORMAT_YVU411, "YVU411"}, {WL_SHM_FORMAT_YUV420, "YUV420"}, {WL_SHM_FORMAT_YVU420, "YVU420"}, {WL_SHM_FORMAT_YUV422, "YUV422"}, {WL_SHM_FORMAT_YVU422, "YVU422"}, {WL_SHM_FORMAT_YUV444, "YUV444"}, {WL_SHM_FORMAT_YVU444, "YVU444"}, {WL_SHM_FORMAT_R8, "R8"}, {WL_SHM_FORMAT_R16, "R16"}, {WL_SHM_FORMAT_RG88, "RG88"}, {WL_SHM_FORMAT_GR88, "GR88"}, {WL_SHM_FORMAT_RG1616, "RG1616"}, {WL_SHM_FORMAT_GR1616, "GR1616"}, {WL_SHM_FORMAT_XRGB16161616F, "XRGB16161616F"}, {WL_SHM_FORMAT_XBGR16161616F, "XBGR16161616F"}, {WL_SHM_FORMAT_ARGB16161616F, "ARGB16161616F"}, {WL_SHM_FORMAT_ABGR16161616F, "ABGR16161616F"}, {WL_SHM_FORMAT_XYUV8888, "XYUV8888"}, {WL_SHM_FORMAT_VUY888, "VUY888"}, {WL_SHM_FORMAT_VUY101010, "VUY101010"}, {WL_SHM_FORMAT_Y210, "Y210"}, {WL_SHM_FORMAT_Y212, "Y212"}, {WL_SHM_FORMAT_Y216, "Y216"}, {WL_SHM_FORMAT_Y410, "Y410"}, {WL_SHM_FORMAT_Y412, "Y412"}, {WL_SHM_FORMAT_Y416, "Y416"}, {WL_SHM_FORMAT_XVYU2101010, "XVYU2101010"}, {WL_SHM_FORMAT_XVYU12_16161616, "XVYU12_16161616"}, {WL_SHM_FORMAT_XVYU16161616, "XVYU16161616"}, {WL_SHM_FORMAT_Y0L0, "Y0L0"}, {WL_SHM_FORMAT_X0L0, "X0L0"}, {WL_SHM_FORMAT_Y0L2, "Y0L2"}, {WL_SHM_FORMAT_X0L2, "X0L2"}, {WL_SHM_FORMAT_YUV420_8BIT, "YUV420_8BIT"}, {WL_SHM_FORMAT_YUV420_10BIT, "YUV420_10BIT"}, {WL_SHM_FORMAT_XRGB8888_A8, "XRGB8888_A8"}, {WL_SHM_FORMAT_XBGR8888_A8, "XBGR8888_A8"}, {WL_SHM_FORMAT_RGBX8888_A8, "RGBX8888_A8"}, {WL_SHM_FORMAT_BGRX8888_A8, "BGRX8888_A8"}, {WL_SHM_FORMAT_RGB888_A8, "RGB888_A8"}, {WL_SHM_FORMAT_BGR888_A8, "BGR888_A8"}, {WL_SHM_FORMAT_RGB565_A8, "RGB565_A8"}, {WL_SHM_FORMAT_BGR565_A8, "BGR565_A8"}, {WL_SHM_FORMAT_NV24, "NV24"}, {WL_SHM_FORMAT_NV42, "NV42"}, {WL_SHM_FORMAT_P210, "P210"}, {WL_SHM_FORMAT_P010, "P010"}, {WL_SHM_FORMAT_P012, "P012"}, {WL_SHM_FORMAT_P016, "P016"}, {WL_SHM_FORMAT_AXBXGXRX106106106106, "AXBXGXRX106106106106"}, {WL_SHM_FORMAT_NV15, "NV15"}, {WL_SHM_FORMAT_Q410, "Q410"}, {WL_SHM_FORMAT_Q401, "Q401"}, #if WAYLAND_VERSION_MAJOR > 1 || WAYLAND_VERSION_MINOR >= 20 {WL_SHM_FORMAT_XRGB16161616, "XRGB16161616"}, {WL_SHM_FORMAT_XBGR16161616, "XBGR16161616"}, {WL_SHM_FORMAT_ARGB16161616, "ARGB16161616"}, {WL_SHM_FORMAT_ABGR16161616, "ABGR16161616"}, #endif #if WAYLAND_VERSION_MAJOR > 1 || WAYLAND_VERSION_MINOR >= 23 {WL_SHM_FORMAT_C1, "C1"}, {WL_SHM_FORMAT_C2, "C2"}, {WL_SHM_FORMAT_C4, "C4"}, {WL_SHM_FORMAT_D1, "D1"}, {WL_SHM_FORMAT_D2, "D2"}, {WL_SHM_FORMAT_D4, "D4"}, {WL_SHM_FORMAT_D8, "D8"}, {WL_SHM_FORMAT_R1, "R1"}, {WL_SHM_FORMAT_R2, "R2"}, {WL_SHM_FORMAT_R4, "R4"}, {WL_SHM_FORMAT_R10, "R10"}, {WL_SHM_FORMAT_R12, "R12"}, {WL_SHM_FORMAT_AVUY8888, "AVUY8888"}, {WL_SHM_FORMAT_XVUY8888, "XVUY8888"}, {WL_SHM_FORMAT_P030, "P030"}, #endif }; #endif foot-1.21.0/shm.c000066400000000000000000000714571476600145200135200ustar00rootroot00000000000000#include "shm.h" #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "shm" #define LOG_ENABLE_DBG 0 #include "log.h" #include "debug.h" #include "macros.h" #include "xmalloc.h" #if !defined(MAP_UNINITIALIZED) #define MAP_UNINITIALIZED 0 #endif #if !defined(MFD_NOEXEC_SEAL) #define MFD_NOEXEC_SEAL 0 #endif #define TIME_SCROLL 0 #define FORCED_DOUBLE_BUFFERING 0 /* * Maximum memfd size allowed. * * On 64-bit, we could in theory use up to 2GB (wk_shm_create_pool() * is limited to int32_t), since we never mmap() the entire region. * * The compositor is different matter - it needs to mmap() the entire * range, and *keep* the mapping for as long as is has buffers * referencing it (thus - always). And if we open multiple terminals, * then the required address space multiples... * * That said, 128TB (the total amount of available user address space * on 64-bit) is *a lot*; we can fit 67108864 2GB memfds into * that. But, let's be conservative for now. * * On 32-bit the available address space is too small and SHM * scrolling is disabled. * * Note: this is the _default_ size. It can be overridden by calling * shm_set_max_pool_size(); */ static off_t max_pool_size = 512 * 1024 * 1024; static bool can_punch_hole = false; static bool can_punch_hole_initialized = false; struct buffer_pool { int fd; /* memfd */ struct wl_shm_pool *wl_pool; void *real_mmapped; /* Address returned from mmap */ size_t mmap_size; /* Size of mmap (>= size) */ size_t ref_count; }; struct buffer_chain; struct buffer_private { struct buffer public; struct buffer_chain *chain; size_t ref_count; bool busy; /* Owned by compositor */ struct buffer_pool *pool; off_t offset; /* Offset into memfd where data begins */ size_t size; bool with_alpha; bool scrollable; }; struct buffer_chain { tll(struct buffer_private *) bufs; struct wl_shm *shm; size_t pix_instances; bool scrollable; pixman_format_code_t pixman_fmt_without_alpha; enum wl_shm_format shm_format_without_alpha; pixman_format_code_t pixman_fmt_with_alpha; enum wl_shm_format shm_format_with_alpha; }; static tll(struct buffer_private *) deferred; #undef MEASURE_SHM_ALLOCS #if defined(MEASURE_SHM_ALLOCS) static size_t max_alloced = 0; #endif void shm_set_max_pool_size(off_t _max_pool_size) { max_pool_size = _max_pool_size; } static void buffer_destroy_dont_close(struct buffer *buf) { if (buf->pix != NULL) { for (size_t i = 0; i < buf->pix_instances; i++) if (buf->pix[i] != NULL) pixman_image_unref(buf->pix[i]); } if (buf->wl_buf != NULL) wl_buffer_destroy(buf->wl_buf); free(buf->pix); buf->pix = NULL; buf->wl_buf = NULL; buf->data = NULL; } static void pool_unref(struct buffer_pool *pool) { if (pool == NULL) return; xassert(pool->ref_count > 0); pool->ref_count--; if (pool->ref_count > 0) return; if (pool->real_mmapped != MAP_FAILED) munmap(pool->real_mmapped, pool->mmap_size); if (pool->wl_pool != NULL) wl_shm_pool_destroy(pool->wl_pool); if (pool->fd >= 0) close(pool->fd); pool->real_mmapped = MAP_FAILED; pool->wl_pool = NULL; pool->fd = -1; free(pool); } static void buffer_destroy(struct buffer_private *buf) { buffer_destroy_dont_close(&buf->public); pool_unref(buf->pool); buf->pool = NULL; for (size_t i = 0; i < buf->public.pix_instances; i++) pixman_region32_fini(&buf->public.dirty[i]); free(buf->public.dirty); free(buf); } static bool buffer_unref_no_remove_from_chain(struct buffer_private *buf) { xassert(buf->ref_count > 0); buf->ref_count--; if (buf->ref_count > 0) return false; if (buf->busy) tll_push_back(deferred, buf); else buffer_destroy(buf); return true; } void shm_fini(void) { LOG_DBG("deferred buffers: %zu", tll_length(deferred)); tll_foreach(deferred, it) { buffer_destroy(it->item); tll_remove(deferred, it); } #if defined(MEASURE_SHM_ALLOCS) && MEASURE_SHM_ALLOCS LOG_INFO("max total allocations was: %zu MB", max_alloced / 1024 / 1024); #endif } static void buffer_release(void *data, struct wl_buffer *wl_buffer) { struct buffer_private *buffer = data; xassert(buffer->public.wl_buf == wl_buffer); xassert(buffer->busy); buffer->busy = false; if (buffer->ref_count == 0) { bool found = false; tll_foreach(deferred, it) { if (it->item == buffer) { found = true; tll_remove(deferred, it); break; } } buffer_destroy(buffer); xassert(found); if (!found) LOG_WARN("deferred delete: buffer not on the 'deferred' list"); } } static const struct wl_buffer_listener buffer_listener = { .release = &buffer_release, }; #if __SIZEOF_POINTER__ == 8 static size_t page_size(void) { static size_t size = 0; if (size == 0) { long n = sysconf(_SC_PAGE_SIZE); if (n <= 0) { LOG_ERRNO("failed to get page size"); size = 4096; } else { size = (size_t)n; } } xassert(size > 0); return size; } #endif static bool instantiate_offset(struct buffer_private *buf, off_t new_offset) { xassert(buf->public.data == NULL); xassert(buf->public.pix == NULL); xassert(buf->public.wl_buf == NULL); xassert(buf->pool != NULL); const struct buffer_pool *pool = buf->pool; void *mmapped = MAP_FAILED; struct wl_buffer *wl_buf = NULL; pixman_image_t **pix = xcalloc(buf->public.pix_instances, sizeof(pix[0])); mmapped = (uint8_t *)pool->real_mmapped + new_offset; wl_buf = wl_shm_pool_create_buffer( pool->wl_pool, new_offset, buf->public.width, buf->public.height, buf->public.stride, buf->with_alpha ? buf->chain->shm_format_with_alpha : buf->chain->shm_format_without_alpha); if (wl_buf == NULL) { LOG_ERR("failed to create SHM buffer"); goto err; } /* One pixman image for each worker thread (do we really need multiple?) */ for (size_t i = 0; i < buf->public.pix_instances; i++) { pix[i] = pixman_image_create_bits_no_clear( buf->with_alpha ? buf->chain->pixman_fmt_with_alpha : buf->chain->pixman_fmt_without_alpha, buf->public.width, buf->public.height, (uint32_t *)mmapped, buf->public.stride); if (pix[i] == NULL) { LOG_ERR("failed to create pixman image"); goto err; } } buf->public.data = mmapped; buf->public.wl_buf = wl_buf; buf->public.pix = pix; buf->offset = new_offset; wl_buffer_add_listener(wl_buf, &buffer_listener, buf); return true; err: if (pix != NULL) { for (size_t i = 0; i < buf->public.pix_instances; i++) if (pix[i] != NULL) pixman_image_unref(pix[i]); } free(pix); if (wl_buf != NULL) wl_buffer_destroy(wl_buf); abort(); return false; } static void NOINLINE get_new_buffers(struct buffer_chain *chain, size_t count, int widths[static count], int heights[static count], struct buffer *bufs[static count], bool with_alpha, bool immediate_purge) { xassert(count == 1 || !chain->scrollable); /* * No existing buffer available. Create a new one by: * * 1. open a memory backed "file" with memfd_create() * 2. mmap() the memory file, to be used by the pixman image * 3. create a wayland shm buffer for the same memory file * * The pixman image and the wayland buffer are now sharing memory. */ int stride[count]; int sizes[count]; size_t total_size = 0; for (size_t i = 0; i < count; i++) { stride[i] = stride_for_format_and_width( with_alpha ? PIXMAN_a8r8g8b8 : PIXMAN_x8r8g8b8, widths[i]); sizes[i] = stride[i] * heights[i]; total_size += sizes[i]; } if (total_size == 0) return; int pool_fd = -1; void *real_mmapped = MAP_FAILED; struct wl_shm_pool *wl_pool = NULL; struct buffer_pool *pool = NULL; /* Backing memory for SHM */ #if defined(MEMFD_CREATE) /* * Older kernels reject MFD_NOEXEC_SEAL with EINVAL. Try first * *with* it, and if that fails, try again *without* it. */ errno = 0; pool_fd = memfd_create( "foot-wayland-shm-buffer-pool", MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL); if (pool_fd < 0 && errno == EINVAL && MFD_NOEXEC_SEAL != 0) { pool_fd = memfd_create( "foot-wayland-shm-buffer-pool", MFD_CLOEXEC | MFD_ALLOW_SEALING); } #elif defined(__FreeBSD__) // memfd_create on FreeBSD 13 is SHM_ANON without sealing support pool_fd = shm_open(SHM_ANON, O_RDWR | O_CLOEXEC, 0600); #else char name[] = "/tmp/foot-wayland-shm-buffer-pool-XXXXXX"; pool_fd = mkostemp(name, O_CLOEXEC); unlink(name); #endif if (pool_fd == -1) { LOG_ERRNO("failed to create SHM backing memory file"); goto err; } #if __SIZEOF_POINTER__ == 8 off_t offset = chain->scrollable && max_pool_size > 0 ? (max_pool_size / 4) & ~(page_size() - 1) : 0; off_t memfd_size = chain->scrollable && max_pool_size > 0 ? max_pool_size : total_size; #else off_t offset = 0; off_t memfd_size = total_size; #endif xassert(chain->scrollable || (offset == 0 && memfd_size == total_size)); LOG_DBG("memfd-size: %lu, initial offset: %lu", memfd_size, offset); if (ftruncate(pool_fd, memfd_size) == -1) { LOG_ERRNO("failed to set size of SHM backing memory file"); goto err; } if (!can_punch_hole_initialized) { can_punch_hole_initialized = true; #if __SIZEOF_POINTER__ == 8 && defined(FALLOC_FL_PUNCH_HOLE) can_punch_hole = fallocate( pool_fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0, 1) == 0; if (!can_punch_hole) { LOG_WARN( "fallocate(FALLOC_FL_PUNCH_HOLE) not " "supported (%s): expect lower performance", strerror(errno)); } #else /* This is mostly to make sure we skip the warning issued * above */ can_punch_hole = false; #endif } if (chain->scrollable && !can_punch_hole) { offset = 0; memfd_size = total_size; chain->scrollable = false; if (ftruncate(pool_fd, memfd_size) < 0) { LOG_ERRNO("failed to set size of SHM backing memory file"); goto err; } } real_mmapped = mmap( NULL, memfd_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_UNINITIALIZED, pool_fd, 0); if (real_mmapped == MAP_FAILED) { LOG_ERRNO("failed to mmap SHM backing memory file"); goto err; } #if defined(MEMFD_CREATE) /* Seal file - we no longer allow any kind of resizing */ /* TODO: wayland mmaps(PROT_WRITE), for some unknown reason, hence we cannot use F_SEAL_FUTURE_WRITE */ if (fcntl(pool_fd, F_ADD_SEALS, F_SEAL_GROW | F_SEAL_SHRINK | /*F_SEAL_FUTURE_WRITE |*/ F_SEAL_SEAL) < 0) { LOG_ERRNO("failed to seal SHM backing memory file"); /* This is not a fatal error */ } #endif wl_pool = wl_shm_create_pool(chain->shm, pool_fd, memfd_size); if (wl_pool == NULL) { LOG_ERR("failed to create SHM pool"); goto err; } pool = xmalloc(sizeof(*pool)); if (pool == NULL) { LOG_ERRNO("failed to allocate buffer pool"); goto err; } *pool = (struct buffer_pool){ .fd = pool_fd, .wl_pool = wl_pool, .real_mmapped = real_mmapped, .mmap_size = memfd_size, .ref_count = 0, }; for (size_t i = 0; i < count; i++) { if (sizes[i] == 0) { bufs[i] = NULL; continue; } /* Push to list of available buffers, but marked as 'busy' */ struct buffer_private *buf = xmalloc(sizeof(*buf)); *buf = (struct buffer_private){ .public = { .width = widths[i], .height = heights[i], .stride = stride[i], .pix_instances = chain->pix_instances, .age = 1234, /* Force a full repaint */ }, .chain = chain, .ref_count = immediate_purge ? 0 : 1, .busy = true, .with_alpha = with_alpha, .pool = pool, .offset = 0, .size = sizes[i], .scrollable = chain->scrollable, }; if (!instantiate_offset(buf, offset)) { free(buf); goto err; } if (immediate_purge) tll_push_front(deferred, buf); else tll_push_front(chain->bufs, buf); buf->public.dirty = xmalloc( chain->pix_instances * sizeof(buf->public.dirty[0])); for (size_t j = 0; j < chain->pix_instances; j++) pixman_region32_init(&buf->public.dirty[j]); pool->ref_count++; offset += buf->size; bufs[i] = &buf->public; } #if defined(MEASURE_SHM_ALLOCS) && MEASURE_SHM_ALLOCS { size_t currently_alloced = 0; tll_foreach(buffers, it) currently_alloced += it->item.size; if (currently_alloced > max_alloced) max_alloced = currently_alloced; } #endif if (!(bufs[0] && shm_can_scroll(bufs[0]))) { /* We only need to keep the pool FD open if we're going to SHM * scroll it */ close(pool_fd); pool->fd = -1; } return; err: pool_unref(pool); if (wl_pool != NULL) wl_shm_pool_destroy(wl_pool); if (real_mmapped != MAP_FAILED) munmap(real_mmapped, memfd_size); if (pool_fd != -1) close(pool_fd); /* We don't handle this */ abort(); } void shm_did_not_use_buf(struct buffer *_buf) { struct buffer_private *buf = (struct buffer_private *)_buf; buf->busy = false; } void shm_get_many(struct buffer_chain *chain, size_t count, int widths[static count], int heights[static count], struct buffer *bufs[static count], bool with_alpha) { get_new_buffers(chain, count, widths, heights, bufs, with_alpha, true); } struct buffer * shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alpha) { LOG_DBG( "chain=%p: looking for a reusable %dx%d buffer " "among %zu potential buffers", (void *)chain, width, height, tll_length(chain->bufs)); struct buffer_private *cached = NULL; tll_foreach(chain->bufs, it) { struct buffer_private *buf = it->item; if (buf->public.width != width || buf->public.height != height || with_alpha != buf->with_alpha) { LOG_DBG("purging mismatching buffer %p", (void *)buf); if (buffer_unref_no_remove_from_chain(buf)) tll_remove(chain->bufs, it); continue; } if (buf->busy) buf->public.age++; else #if FORCED_DOUBLE_BUFFERING if (buf->public.age == 0) buf->public.age++; else #endif { if (cached == NULL) cached = buf; else { /* We have multiple buffers eligible for * reuse. Pick the "youngest" one, and mark the * other one for purging */ if (buf->public.age < cached->public.age) { shm_unref(&cached->public); cached = buf; } else { /* * TODO: I think we _can_ use shm_unref() * here... * * shm_unref() may remove 'it', but that * should be safe; "our" tll_foreach() already * holds the next pointer. */ if (buffer_unref_no_remove_from_chain(buf)) tll_remove(chain->bufs, it); } } } } if (cached != NULL) { LOG_DBG("reusing buffer %p from cache", (void *)cached); cached->busy = true; for (size_t i = 0; i < cached->public.pix_instances; i++) pixman_region32_clear(&cached->public.dirty[i]); xassert(cached->public.pix_instances == chain->pix_instances); return &cached->public; } struct buffer *ret; get_new_buffers(chain, 1, &width, &height, &ret, with_alpha, false); return ret; } bool shm_can_scroll(const struct buffer *_buf) { #if __SIZEOF_POINTER__ == 8 const struct buffer_private *buf = (const struct buffer_private *)_buf; return can_punch_hole && max_pool_size > 0 && buf->scrollable; #else /* Not enough virtual address space in 32-bit */ return false; #endif } #if __SIZEOF_POINTER__ == 8 && defined(FALLOC_FL_PUNCH_HOLE) static bool wrap_buffer(struct buffer_private *buf, off_t new_offset) { struct buffer_pool *pool = buf->pool; xassert(pool->ref_count == 1); /* We don't allow overlapping offsets */ off_t UNUSED diff = new_offset < buf->offset ? buf->offset - new_offset : new_offset - buf->offset; xassert(diff > buf->size); memcpy((uint8_t *)pool->real_mmapped + new_offset, buf->public.data, buf->size); off_t trim_ofs, trim_len; if (new_offset > buf->offset) { /* Trim everything *before* the new offset */ trim_ofs = 0; trim_len = new_offset; } else { /* Trim everything *after* the new buffer location */ trim_ofs = new_offset + buf->size; trim_len = pool->mmap_size - trim_ofs; } if (fallocate( pool->fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, trim_ofs, trim_len) < 0) { LOG_ERRNO("failed to trim SHM backing memory file"); return false; } /* Re-instantiate pixman+wl_buffer+raw pointersw */ buffer_destroy_dont_close(&buf->public); return instantiate_offset(buf, new_offset); } static bool shm_scroll_forward(struct buffer_private *buf, int rows, int top_margin, int top_keep_rows, int bottom_margin, int bottom_keep_rows) { struct buffer_pool *pool = buf->pool; xassert(can_punch_hole); xassert(buf->busy); xassert(buf->public.pix != NULL); xassert(buf->public.wl_buf != NULL); xassert(pool != NULL); xassert(pool->ref_count == 1); xassert(pool->fd >= 0); LOG_DBG("scrolling %d rows (%d bytes)", rows, rows * buf->public.stride); const off_t diff = rows * buf->public.stride; xassert(rows > 0); xassert(diff < buf->size); if (buf->offset + diff + buf->size > max_pool_size) { LOG_DBG("memfd offset wrap around"); if (!wrap_buffer(buf, 0)) goto err; } off_t new_offset = buf->offset + diff; xassert(new_offset > buf->offset); xassert(new_offset + buf->size <= max_pool_size); #if TIME_SCROLL struct timespec tot; struct timespec time1; clock_gettime(CLOCK_MONOTONIC, &time1); struct timespec time2 = time1; #endif if (top_keep_rows > 0) { /* Copy current 'top' region to its new location */ const int stride = buf->public.stride; uint8_t *base = buf->public.data; memmove( base + (top_margin + rows) * stride, base + (top_margin + 0) * stride, top_keep_rows * stride); #if TIME_SCROLL clock_gettime(CLOCK_MONOTONIC, &time2); timespec_sub(&time2, &time1, &tot); LOG_INFO("memmove (top region): %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); #endif } /* Destroy old objects (they point to the old offset) */ buffer_destroy_dont_close(&buf->public); /* Free unused memory - everything up until the new offset */ const off_t trim_ofs = 0; const off_t trim_len = new_offset; if (fallocate( pool->fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, trim_ofs, trim_len) < 0) { LOG_ERRNO("failed to trim SHM backing memory file"); goto err; } #if TIME_SCROLL struct timespec time3; clock_gettime(CLOCK_MONOTONIC, &time3); timespec_sub(&time3, &time2, &tot); LOG_INFO("PUNCH HOLE: %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); #endif /* Re-instantiate pixman+wl_buffer+raw pointersw */ bool ret = instantiate_offset(buf, new_offset); #if TIME_SCROLL struct timespec time4; clock_gettime(CLOCK_MONOTONIC, &time4); timespec_sub(&time4, &time3, &tot); LOG_INFO("instantiate offset: %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); #endif if (ret && bottom_keep_rows > 0) { /* Copy 'bottom' region to its new location */ const size_t size = buf->size; const int stride = buf->public.stride; uint8_t *base = buf->public.data; memmove( base + size - (bottom_margin + bottom_keep_rows) * stride, base + size - (bottom_margin + rows + bottom_keep_rows) * stride, bottom_keep_rows * stride); #if TIME_SCROLL struct timespec time5; clock_gettime(CLOCK_MONOTONIC, &time5); timespec_sub(&time5, &time4, &tot); LOG_INFO("memmove (bottom region): %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); #endif } return ret; err: abort(); return false; } static bool shm_scroll_reverse(struct buffer_private *buf, int rows, int top_margin, int top_keep_rows, int bottom_margin, int bottom_keep_rows) { xassert(rows > 0); struct buffer_pool *pool = buf->pool; xassert(pool->ref_count == 1); const off_t diff = rows * buf->public.stride; if (diff > buf->offset) { LOG_DBG("memfd offset reverse wrap-around"); if (!wrap_buffer(buf, (max_pool_size - buf->size) & ~(page_size() - 1))) goto err; } off_t new_offset = buf->offset - diff; xassert(new_offset < buf->offset); xassert(new_offset <= max_pool_size); #if TIME_SCROLL struct timespec time0; clock_gettime(CLOCK_MONOTONIC, &time0); struct timespec tot; struct timespec time1 = time0; #endif if (bottom_keep_rows > 0) { /* Copy 'bottom' region to its new location */ const size_t size = buf->size; const int stride = buf->public.stride; uint8_t *base = buf->public.data; memmove( base + size - (bottom_margin + rows + bottom_keep_rows) * stride, base + size - (bottom_margin + bottom_keep_rows) * stride, bottom_keep_rows * stride); #if TIME_SCROLL clock_gettime(CLOCK_MONOTONIC, &time1); timespec_sub(&time1, &time0, &tot); LOG_INFO("memmove (bottom region): %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); #endif } /* Destroy old objects (they point to the old offset) */ buffer_destroy_dont_close(&buf->public); /* Free unused memory - everything after the relocated buffer */ const off_t trim_ofs = new_offset + buf->size; const off_t trim_len = pool->mmap_size - trim_ofs; if (fallocate( pool->fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, trim_ofs, trim_len) < 0) { LOG_ERRNO("failed to trim SHM backing memory"); goto err; } #if TIME_SCROLL struct timespec time2; clock_gettime(CLOCK_MONOTONIC, &time2); timespec_sub(&time2, &time1, &tot); LOG_INFO("fallocate: %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); #endif /* Re-instantiate pixman+wl_buffer+raw pointers */ bool ret = instantiate_offset(buf, new_offset); #if TIME_SCROLL struct timespec time3; clock_gettime(CLOCK_MONOTONIC, &time3); timespec_sub(&time3, &time2, &tot); LOG_INFO("instantiate offset: %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); #endif if (ret && top_keep_rows > 0) { /* Copy current 'top' region to its new location */ const int stride = buf->public.stride; uint8_t *base = buf->public.data; memmove( base + (top_margin + 0) * stride, base + (top_margin + rows) * stride, top_keep_rows * stride); #if TIME_SCROLL struct timespec time4; clock_gettime(CLOCK_MONOTONIC, &time4); timespec_sub(&time4, &time3, &tot); LOG_INFO("memmove (top region): %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); #endif } return ret; err: abort(); return false; } #endif /* FALLOC_FL_PUNCH_HOLE */ bool shm_scroll(struct buffer *_buf, int rows, int top_margin, int top_keep_rows, int bottom_margin, int bottom_keep_rows) { #if __SIZEOF_POINTER__ == 8 && defined(FALLOC_FL_PUNCH_HOLE) if (!shm_can_scroll(_buf)) return false; struct buffer_private *buf = (struct buffer_private *)_buf; xassert(rows != 0); return rows > 0 ? shm_scroll_forward(buf, rows, top_margin, top_keep_rows, bottom_margin, bottom_keep_rows) : shm_scroll_reverse(buf, -rows, top_margin, top_keep_rows, bottom_margin, bottom_keep_rows); #else return false; #endif } void shm_purge(struct buffer_chain *chain) { LOG_DBG("chain: %p: purging all buffers", (void *)chain); /* Purge old buffers associated with this cookie */ tll_foreach(chain->bufs, it) { if (buffer_unref_no_remove_from_chain(it->item)) tll_remove(chain->bufs, it); } } void shm_addref(struct buffer *_buf) { struct buffer_private *buf = (struct buffer_private *)_buf; buf->ref_count++; } void shm_unref(struct buffer *_buf) { if (_buf == NULL) return; struct buffer_private *buf = (struct buffer_private *)_buf; struct buffer_chain *chain = buf->chain; tll_foreach(chain->bufs, it) { if (it->item != buf) continue; if (buffer_unref_no_remove_from_chain(buf)) tll_remove(chain->bufs, it); break; } } struct buffer_chain * shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, bool ten_bit_if_capable) { pixman_format_code_t pixman_fmt_without_alpha = PIXMAN_x8r8g8b8; enum wl_shm_format shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB8888; pixman_format_code_t pixman_fmt_with_alpha = PIXMAN_a8r8g8b8; enum wl_shm_format shm_fmt_with_alpha = WL_SHM_FORMAT_ARGB8888; static bool have_logged = false; if (ten_bit_if_capable) { if (wayl->shm_have_argb2101010 && wayl->shm_have_xrgb2101010) { pixman_fmt_without_alpha = PIXMAN_x2r10g10b10; shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB2101010; pixman_fmt_with_alpha = PIXMAN_a2r10g10b10; shm_fmt_with_alpha = WL_SHM_FORMAT_ARGB2101010; if (!have_logged) { have_logged = true; LOG_INFO("using 10-bit RGB surfaces"); } } else if (wayl->shm_have_abgr2101010 && wayl->shm_have_xbgr2101010) { pixman_fmt_without_alpha = PIXMAN_x2b10g10r10; shm_fmt_without_alpha = WL_SHM_FORMAT_XBGR2101010; pixman_fmt_with_alpha = PIXMAN_a2b10g10r10; shm_fmt_with_alpha = WL_SHM_FORMAT_ABGR2101010; if (!have_logged) { have_logged = true; LOG_INFO("using 10-bit BGR surfaces"); } } else { if (!have_logged) { have_logged = true; LOG_WARN( "10-bit surfaces requested, but compositor does not " "implement ARGB2101010+XRGB2101010, or " "ABGR2101010+XBGR2101010. Falling back to 8-bit surfaces"); } } } else { if (!have_logged) { have_logged = true; LOG_INFO("using 8-bit RGB surfaces"); } } struct buffer_chain *chain = xmalloc(sizeof(*chain)); *chain = (struct buffer_chain){ .bufs = tll_init(), .shm = wayl->shm, .pix_instances = pix_instances, .scrollable = scrollable, .pixman_fmt_without_alpha = pixman_fmt_without_alpha, .shm_format_without_alpha = shm_fmt_without_alpha, .pixman_fmt_with_alpha = pixman_fmt_with_alpha, .shm_format_with_alpha = shm_fmt_with_alpha, }; return chain; } void shm_chain_free(struct buffer_chain *chain) { if (chain == NULL) return; shm_purge(chain); if (tll_length(chain->bufs) > 0) { BUG("chain=%p: there are buffers remaining; " "is there a missing call to shm_unref()?", (void *)chain); } free(chain); } foot-1.21.0/shm.h000066400000000000000000000051041476600145200135070ustar00rootroot00000000000000#pragma once #include #include #include #include #include #include #include "wayland.h" struct damage; struct buffer { int width; int height; int stride; void *data; struct wl_buffer *wl_buf; pixman_image_t **pix; size_t pix_instances; unsigned age; /* * First item in the array is used to track frame-to-frame * damage. This is used when re-applying damage from the last * frame, when the compositor doesn't release buffers immediately * (forcing us to double buffer) * * The remaining items are used to track surface damage. Each * worker thread adds its own cell damage to "its" region. When * the frame is done, all damage is converted to a single region, * which is then used in calls to wl_surface_damage_buffer(). */ pixman_region32_t *dirty; }; void shm_fini(void); void shm_set_max_pool_size(off_t max_pool_size); struct buffer_chain; struct buffer_chain *shm_chain_new( struct wayland *wayl, bool scrollable, size_t pix_instances, bool ten_bit_it_if_capable); void shm_chain_free(struct buffer_chain *chain); /* * Returns a single buffer. * * May returned a cached buffer. If so, the buffer's age indicates how * many shm_get_buffer() calls have been made for the same * width/height while the buffer was still busy. * * A newly allocated buffer has an age of 1234. */ struct buffer *shm_get_buffer( struct buffer_chain *chain, int width, int height, bool with_alpha); /* * Returns many buffers, described by 'info', all sharing the same SHM * buffer pool. * * Never returns cached buffers. However, the newly created buffers * are all inserted into the regular buffer cache, and are treated * just like buffers created by shm_get_buffer(). * * This function is useful when allocating many small buffers, with * (roughly) the same life time. * * Buffers are tagged for immediate purging, and will be destroyed as * soon as the compositor releases them. */ void shm_get_many( struct buffer_chain *chain, size_t count, int widths[static count], int heights[static count], struct buffer *bufs[static count], bool with_alpha); void shm_did_not_use_buf(struct buffer *buf); bool shm_can_scroll(const struct buffer *buf); bool shm_scroll(struct buffer *buf, int rows, int top_margin, int top_keep_rows, int bottom_margin, int bottom_keep_rows); void shm_addref(struct buffer *buf); void shm_unref(struct buffer *buf); void shm_purge(struct buffer_chain *chain); foot-1.21.0/sixel.c000066400000000000000000002135301476600145200140430ustar00rootroot00000000000000#include "sixel.h" #include #include #define LOG_MODULE "sixel" #define LOG_ENABLE_DBG 0 #include "log.h" #include "debug.h" #include "grid.h" #include "hsl.h" #include "render.h" #include "srgb.h" #include "util.h" #include "xmalloc.h" #include "xsnprintf.h" static size_t count; static void sixel_put_generic(struct terminal *term, uint8_t c); static void sixel_put_ar_11(struct terminal *term, uint8_t c); static uint32_t color_decode_srgb(const struct terminal *term, uint16_t r, uint16_t g, uint16_t b) { if (term->sixel.linear_blending) { if (term->sixel.use_10bit) { r = srgb_decode_8_to_16(r) >> 6; g = srgb_decode_8_to_16(g) >> 6; b = srgb_decode_8_to_16(b) >> 6; } else { r = srgb_decode_8_to_8(r); g = srgb_decode_8_to_8(g); b = srgb_decode_8_to_8(b); } } else { if (term->sixel.use_10bit) { r <<= 2; g <<= 2; b <<= 2; } } uint32_t color; if (term->sixel.use_10bit) { if (PIXMAN_FORMAT_TYPE(term->sixel.pixman_fmt) == PIXMAN_TYPE_ARGB) color = 0x3u << 30 | r << 20 | g << 10 | b; else color = 0x3u << 30 | b << 20 | g << 10 | r; } else color = 0xffu << 24 | r << 16 | g << 8 | b; return color; } void sixel_fini(struct terminal *term) { free(term->sixel.image.data); free(term->sixel.private_palette); free(term->sixel.shared_palette); } sixel_put sixel_init(struct terminal *term, int p1, int p2, int p3) { /* * P1: pixel aspect ratio * - 0,1 - 2:1 * - 2 - 5:1 * - 3,4 - 3:1 * - 5,6 - 2:1 * - 7,8,9 - 1:1 * * P2: background color mode * - 0|2: empty pixels use current background color * - 1: empty pixels remain at their current color (i.e. transparent) * P3: horizontal grid size - ignored */ xassert(term->sixel.image.data == NULL); xassert(term->sixel.palette_size <= SIXEL_MAX_COLORS); /* Default aspect ratio is 2:1 */ const int pad = 1; const int pan = (p1 == 2) ? 5 : (p1 == 3 || p1 == 4) ? 3 : (p1 == 7 || p1 == 8 || p1 == 9) ? 1 : 2; LOG_DBG("initializing sixel with " "p1=%d (pan=%d, pad=%d, aspect-ratio=%d:%d), " "p2=%d (transparent=%s), " "p3=%d (ignored)", p1, pan, pad, pan, pad, p2, p2 == 1 ? "yes" : "no", p3); term->sixel.state = SIXEL_DECSIXEL; term->sixel.pos = (struct coord){0, 0}; term->sixel.color_idx = 0; term->sixel.pan = pan; term->sixel.pad = pad; term->sixel.param = 0; term->sixel.param_idx = 0; memset(term->sixel.params, 0, sizeof(term->sixel.params)); term->sixel.transparent_bg = p2 == 1; term->sixel.image.data = NULL; term->sixel.image.p = NULL; term->sixel.image.width = 0; term->sixel.image.height = 0; term->sixel.image.alloc_height = 0; term->sixel.image.bottom_pixel = 0; term->sixel.linear_blending = render_do_linear_blending(term); term->sixel.pixman_fmt = PIXMAN_a8r8g8b8; if (term->conf->tweak.surface_bit_depth == SHM_10_BIT) { if (term->wl->shm_have_argb2101010 && term->wl->shm_have_xrgb2101010) { term->sixel.use_10bit = true; term->sixel.pixman_fmt = PIXMAN_a2r10g10b10; } else if (term->wl->shm_have_abgr2101010 && term->wl->shm_have_xbgr2101010) { term->sixel.use_10bit = true; term->sixel.pixman_fmt = PIXMAN_a2b10g10r10; } } const size_t active_palette_entries = min( ALEN(term->conf->colors.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.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.sixel, active_palette_entries * sizeof(term->sixel.shared_palette[0])); if (term->sixel.linear_blending || term->sixel.use_10bit) { for (size_t i = 0; i < active_palette_entries; i++) { uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff; uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff; uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff; term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b); } } } else { /* Shared palette - do *not* reset palette for new sixels */ } term->sixel.palette = term->sixel.shared_palette; } count = 0; return pan == 1 && pad == 1 ? &sixel_put_ar_11 : &sixel_put_generic; } static void sixel_invalidate_cache(struct sixel *sixel) { if (sixel->scaled.pix != NULL) pixman_image_unref(sixel->scaled.pix); free(sixel->scaled.data); sixel->scaled.pix = NULL; sixel->scaled.data = NULL; sixel->scaled.width = -1; sixel->scaled.height = -1; sixel->pix = NULL; sixel->width = -1; sixel->height = -1; } void sixel_destroy(struct sixel *sixel) { sixel_invalidate_cache(sixel); if (sixel->original.pix != NULL) pixman_image_unref(sixel->original.pix); free(sixel->original.data); sixel->original.pix = NULL; sixel->original.data = NULL; } void sixel_destroy_all(struct terminal *term) { tll_foreach(term->normal.sixel_images, it) sixel_destroy(&it->item); tll_foreach(term->alt.sixel_images, it) sixel_destroy(&it->item); tll_free(term->normal.sixel_images); tll_free(term->alt.sixel_images); } static void sixel_erase(struct terminal *term, struct sixel *sixel) { for (int i = 0; i < sixel->rows; i++) { int r = (sixel->pos.row + i) & (term->grid->num_rows - 1); struct row *row = term->grid->rows[r]; if (row == NULL) { /* A resize/reflow may cause row to now be unallocated */ continue; } row->dirty = true; for (int c = sixel->pos.col; c < min(sixel->pos.col + sixel->cols, term->cols); c++) row->cells[c].attrs.clean = 0; } sixel_destroy(sixel); } /* * Verify the sixels are sorted correctly. * * The sixels are sorted on their *end* row, in descending order. This * invariant means the most recent sixels appear first in the list. */ static void verify_list_order(const struct terminal *term) { #if defined(_DEBUG) int prev_row = INT_MAX; int prev_col = -1; int prev_col_count = 0; /* To aid debugging */ size_t UNUSED idx = 0; tll_foreach(term->grid->sixel_images, it) { int row = grid_row_abs_to_sb( term->grid, term->rows, it->item.pos.row + it->item.rows - 1); int col = it->item.pos.col; int col_count = it->item.cols; xassert(row <= prev_row); if (row == prev_row) { /* Allowed to be on the same row only if their columns * don't overlap */ xassert(col + col_count <= prev_col || prev_col + prev_col_count <= col); } prev_row = row; prev_col = col; prev_col_count = col_count; idx++; } #endif } /* * Verifies there aren't any sixels that cross the scrollback * wrap-around. This invariant means a sixel's absolute row numbers * are strictly increasing. */ static void verify_no_wraparound_crossover(const struct terminal *term) { #if defined(_DEBUG) tll_foreach(term->grid->sixel_images, it) { const struct sixel *six = &it->item; xassert(six->pos.row >= 0); xassert(six->pos.row < term->grid->num_rows); int end = (six->pos.row + six->rows - 1) & (term->grid->num_rows - 1); xassert(end >= six->pos.row); } #endif } /* * Verify there aren't any sixels that cross the scrollback end. This * invariant means a sixel's rebased row numbers are strictly * increasing. */ static void verify_scrollback_consistency(const struct terminal *term) { #if defined(_DEBUG) tll_foreach(term->grid->sixel_images, it) { const struct sixel *six = &it->item; int last_row = -1; for (int i = 0; i < six->rows; i++) { int row_no = grid_row_abs_to_sb( term->grid, term->rows, six->pos.row + i); if (last_row != -1) xassert(last_row < row_no); last_row = row_no; } } #endif } /* * Verifies no sixel overlap with any other sixels. */ static void verify_no_overlap(const struct terminal *term) { #if defined(_DEBUG) tll_foreach(term->grid->sixel_images, it) { const struct sixel *six1 = &it->item; pixman_region32_t rect1; pixman_region32_init_rect( &rect1, six1->pos.col, six1->pos.row, six1->cols, six1->rows); tll_foreach(term->grid->sixel_images, it2) { const struct sixel *six2 = &it2->item; if (six1 == six2) continue; pixman_region32_t rect2; pixman_region32_init_rect( &rect2, six2->pos.col, six2->pos.row, six2->cols, six2->rows); pixman_region32_t intersection; pixman_region32_init(&intersection); pixman_region32_intersect(&intersection, &rect1, &rect2); xassert(!pixman_region32_not_empty(&intersection)); pixman_region32_fini(&intersection); pixman_region32_fini(&rect2); } pixman_region32_fini(&rect1); } #endif } static void verify_sixels(const struct terminal *term) { verify_no_wraparound_crossover(term); verify_scrollback_consistency(term); verify_no_overlap(term); verify_list_order(term); } static void sixel_insert(struct terminal *term, struct sixel sixel) { int end_row = grid_row_abs_to_sb( term->grid, term->rows, sixel.pos.row + sixel.rows - 1); tll_foreach(term->grid->sixel_images, it) { int rebased = grid_row_abs_to_sb( term->grid, term->rows, it->item.pos.row + it->item.rows - 1); if (rebased < end_row) { tll_insert_before(term->grid->sixel_images, it, sixel); goto out; } } tll_push_back(term->grid->sixel_images, sixel); out: #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG LOG_DBG("sixel list after insertion:"); tll_foreach(term->grid->sixel_images, it) { LOG_DBG(" rows=%d+%d", it->item.pos.row, it->item.rows); } #endif verify_sixels(term); } void sixel_scroll_up(struct terminal *term, int rows) { if (likely(tll_length(term->grid->sixel_images) == 0)) return; tll_rforeach(term->grid->sixel_images, it) { struct sixel *six = &it->item; int six_start = grid_row_abs_to_sb(term->grid, term->rows, six->pos.row); if (six_start < rows) { sixel_erase(term, six); tll_remove(term->grid->sixel_images, it); } else { /* * Unfortunately, we cannot break here. * * The sixels are sorted on their *end* row. This means * there may be a sixel with a top row that will be * scrolled out *anywhere* in the list (think of a huuuuge * sixel that covers the entire scrollback) */ //break; } } term->bits_affecting_ascii_printer.sixels = tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); verify_sixels(term); } void sixel_scroll_down(struct terminal *term, int rows) { if (likely(tll_length(term->grid->sixel_images) == 0)) return; xassert(term->grid->num_rows >= rows); tll_foreach(term->grid->sixel_images, it) { struct sixel *six = &it->item; int six_end = grid_row_abs_to_sb( term->grid, term->rows, six->pos.row + six->rows - 1); if (six_end >= term->grid->num_rows - rows) { sixel_erase(term, six); tll_remove(term->grid->sixel_images, it); } else break; } term->bits_affecting_ascii_printer.sixels = tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); verify_sixels(term); } static void blend_new_image_over_old(const struct terminal *term, const struct sixel *six, pixman_region32_t *six_rect, int row, int col, pixman_image_t **pix, bool *opaque) { xassert(pix != NULL); xassert(opaque != NULL); /* * TODO: handle images being emitted with different cell dimensions */ const int six_ofs_x = six->pos.col * six->cell_width; const int six_ofs_y = six->pos.row * six->cell_height; const int img_ofs_x = col * six->cell_width; const int img_ofs_y = row * six->cell_height; const int img_width = pixman_image_get_width(*pix); const int img_height = pixman_image_get_height(*pix); pixman_region32_t pix_rect; pixman_region32_init_rect( &pix_rect, img_ofs_x, img_ofs_y, img_width, img_height); /* Blend the intersection between the old and new images */ pixman_region32_t intersection; pixman_region32_init(&intersection); pixman_region32_intersect(&intersection, six_rect, &pix_rect); int n_rects = -1; pixman_box32_t *boxes = pixman_region32_rectangles( &intersection, &n_rects); if (n_rects == 0) goto out; xassert(n_rects == 1); pixman_box32_t *box = &boxes[0]; if (!*opaque) { /* * New image is transparent - blend on top of the old * sixel image. */ pixman_image_composite32( PIXMAN_OP_OVER_REVERSE, six->original.pix, NULL, *pix, box->x1 - six_ofs_x, box->y1 - six_ofs_y, 0, 0, box->x1 - img_ofs_x, box->y1 - img_ofs_y, box->x2 - box->x1, box->y2 - box->y1); } /* * Since the old image is split into sub-tiles on a * per-row basis, we need to enlarge the new image and * copy the old image if the old image extends beyond the * new image. * * The "bounding" coordinates are either the edges of the * old image, or the next cell boundary, whichever comes * first. */ int bounding_x = six_ofs_x + six->original.width > img_ofs_x + img_width ? min( six_ofs_x + six->original.width, (box->x2 + six->cell_width - 1) / six->cell_width * six->cell_width) : box->x2; int bounding_y = six_ofs_y + six->original.height > img_ofs_y + img_height ? min( six_ofs_y + six->original.height, (box->y2 + six->cell_height - 1) / six->cell_height * six->cell_height) : box->y2; /* The required size of the new image */ const int required_width = bounding_x - img_ofs_x; const int required_height = bounding_y - img_ofs_y; const int new_width = max(img_width, required_width); const int new_height = max(img_height, required_height); if (new_width <= img_width && new_height <= img_height) goto out; //LOG_INFO("enlarging: %dx%d -> %dx%d", img_width, img_height, new_width, new_height); if (!six->opaque) { /* Transparency is viral */ *opaque = false; } /* Create a new pixmap */ int stride = new_width * sizeof(uint32_t); uint32_t *new_data = xmalloc(stride * new_height); pixman_image_t *pix2 = pixman_image_create_bits_no_clear( term->sixel.pixman_fmt, new_width, new_height, new_data, stride); #if defined(_DEBUG) /* Fill new image with an easy-to-recognize color (green) */ for (size_t i = 0; i < new_width * new_height; i++) new_data[i] = 0xff00ff00; #endif /* Copy the new image, from its old pixmap, to the new pixmap */ pixman_image_composite32( PIXMAN_OP_SRC, *pix, NULL, pix2, 0, 0, 0, 0, 0, 0, img_width, img_height); /* Copy the bottom tile of the old sixel image into the new pixmap */ pixman_image_composite32( PIXMAN_OP_SRC, six->original.pix, NULL, pix2, box->x1 - six_ofs_x, box->y2 - six_ofs_y, 0, 0, box->x1 - img_ofs_x, box->y2 - img_ofs_y, bounding_x - box->x1, bounding_y - box->y2); /* Copy the right tile of the old sixel image into the new pixmap */ pixman_image_composite32( PIXMAN_OP_SRC, six->original.pix, NULL, pix2, box->x2 - six_ofs_x, box->y1 - six_ofs_y, 0, 0, box->x2 - img_ofs_x, box->y1 - img_ofs_y, bounding_x - box->x2, bounding_y - box->y1); /* * Ensure the newly allocated area is initialized. * * Some of it, or all, will have been initialized above, by the * bottom and right tiles from the old sixel image. However, there * may be areas in the new image that isn't covered by the old * image. These areas need to be made transparent. */ pixman_region32_t uninitialized; pixman_region32_init_rects( &uninitialized, (const pixman_box32_t []){ /* Extended image area on the right side */ {img_ofs_x + img_width, img_ofs_y, img_ofs_x + new_width, img_ofs_y + new_height}, /* Bottom */ {img_ofs_x, img_ofs_y + img_height, img_ofs_x + new_width, img_ofs_y + new_height}}, 2); /* Subtract the old sixel image, since the area(s) covered by the * old image has already been copied, and *must* not be * overwritten */ pixman_region32_t diff; pixman_region32_init(&diff); pixman_region32_subtract(&diff, &uninitialized, six_rect); if (pixman_region32_not_empty(&diff)) { pixman_image_t *src = pixman_image_create_solid_fill(&(pixman_color_t){0}); int count = -1; pixman_box32_t *rects = pixman_region32_rectangles(&diff, &count); for (int i = 0; i < count; i++) { pixman_image_composite32( PIXMAN_OP_SRC, src, NULL, pix2, 0, 0, 0, 0, rects[i].x1 - img_ofs_x, rects[i].y1 - img_ofs_y, rects[i].x2 - rects[i].x1, rects[i].y2 - rects[i].y1); } pixman_image_unref(src); *opaque = false; } pixman_region32_fini(&diff); pixman_region32_fini(&uninitialized); /* Use the new pixmap in place of the old one */ free(pixman_image_get_data(*pix)); pixman_image_unref(*pix); *pix = pix2; out: pixman_region32_fini(&intersection); pixman_region32_fini(&pix_rect); } static void sixel_overwrite(struct terminal *term, struct sixel *six, int row, int col, int height, int width, pixman_image_t **pix, bool *opaque) { pixman_region32_t six_rect; pixman_region32_init_rect( &six_rect, six->pos.col * six->cell_width, six->pos.row * six->cell_height, six->original.width, six->original.height); pixman_region32_t overwrite_rect; pixman_region32_init_rect( &overwrite_rect, col * six->cell_width, row * six->cell_height, width * six->cell_width, height * six->cell_height); #if defined(_DEBUG) pixman_region32_t cell_intersection; pixman_region32_init(&cell_intersection); pixman_region32_intersect(&cell_intersection, &six_rect, &overwrite_rect); xassert(!pixman_region32_not_empty(&six_rect) || pixman_region32_not_empty(&cell_intersection)); pixman_region32_fini(&cell_intersection); #endif if (pix != NULL) blend_new_image_over_old(term, six, &six_rect, row, col, pix, opaque); pixman_region32_t diff; pixman_region32_init(&diff); pixman_region32_subtract(&diff, &six_rect, &overwrite_rect); pixman_region32_fini(&six_rect); pixman_region32_fini(&overwrite_rect); int n_rects = -1; pixman_box32_t *boxes = pixman_region32_rectangles(&diff, &n_rects); for (int i = 0; i < n_rects; i++) { LOG_DBG("box #%d: x1=%d, y1=%d, x2=%d, y2=%d", i, boxes[i].x1, boxes[i].y1, boxes[i].x2, boxes[i].y2); xassert(boxes[i].x1 % six->cell_width == 0); xassert(boxes[i].y1 % six->cell_height == 0); /* New image's position, in cells */ const int new_col = boxes[i].x1 / six->cell_width; const int new_row = boxes[i].y1 / six->cell_height; xassert(new_row < term->grid->num_rows); /* New image's width and height, in pixels */ const int new_width = boxes[i].x2 - boxes[i].x1; const int new_height = boxes[i].y2 - boxes[i].y1; uint32_t *new_data = xmalloc(new_width * new_height * sizeof(uint32_t)); const uint32_t *old_data = six->original.data; /* Pixel offsets into old image backing memory */ const int x_ofs = boxes[i].x1 - six->pos.col * six->cell_width; const int y_ofs = boxes[i].y1 - six->pos.row * six->cell_height; /* Copy image data, one row at a time */ for (size_t j = 0; j < new_height; j++) { memcpy( &new_data[(0 + j) * new_width], &old_data[(y_ofs + j) * six->original.width + x_ofs], new_width * sizeof(uint32_t)); } pixman_image_t *new_pix = pixman_image_create_bits_no_clear( term->sixel.pixman_fmt, new_width, new_height, new_data, new_width * sizeof(uint32_t)); struct sixel new_six = { .pix = NULL, .width = -1, .height = -1, .pos = {.col = new_col, .row = new_row}, .cols = (new_width + six->cell_width - 1) / six->cell_width, .rows = (new_height + six->cell_height - 1) / six->cell_height, .opaque = six->opaque, .cell_width = six->cell_width, .cell_height = six->cell_height, .original = { .data = new_data, .pix = new_pix, .width = new_width, .height = new_height, }, .scaled = { .data = NULL, .pix = NULL, .width = -1, .height = -1, }, }; #if defined(_DEBUG) /* Assert we don't cross the scrollback wrap-around */ const int new_end = new_six.pos.row + new_six.rows - 1; xassert(new_end < term->grid->num_rows); #endif sixel_insert(term, new_six); } pixman_region32_fini(&diff); } /* Row numbers are absolute */ static void _sixel_overwrite_by_rectangle( struct terminal *term, int row, int col, int height, int width, pixman_image_t **pix, bool *opaque) { verify_sixels(term); #if defined(_DEBUG) pixman_region32_t overwrite_rect; pixman_region32_init_rect(&overwrite_rect, col, row, width, height); #endif const int start = row; const int end = row + height - 1; /* We should never generate scrollback wrapping sixels */ xassert(end < term->grid->num_rows); const int scrollback_rel_start = grid_row_abs_to_sb( term->grid, term->rows, start); bool UNUSED would_have_breaked = false; tll_foreach(term->grid->sixel_images, it) { struct sixel *six = &it->item; const int six_start = six->pos.row; const int six_end = (six_start + six->rows - 1); const int six_scrollback_rel_end = grid_row_abs_to_sb(term->grid, term->rows, six_end); /* We should never generate scrollback wrapping sixels */ xassert(six_end < term->grid->num_rows); if (six_scrollback_rel_end < scrollback_rel_start) { /* All remaining sixels are *before* our rectangle */ would_have_breaked = true; break; } #if defined(_DEBUG) pixman_region32_t six_rect; pixman_region32_init_rect(&six_rect, six->pos.col, six->pos.row, six->cols, six->rows); pixman_region32_t intersection; pixman_region32_init(&intersection); pixman_region32_intersect(&intersection, &six_rect, &overwrite_rect); const bool collides = pixman_region32_not_empty(&intersection); #else const bool UNUSED collides = false; #endif if ((start <= six_start && end >= six_start) || /* Crosses sixel start boundary */ (start <= six_end && end >= six_end) || /* Crosses sixel end boundary */ (start >= six_start && end <= six_end)) /* Fully within sixel range */ { const int col_start = six->pos.col; const int col_end = six->pos.col + six->cols - 1; if ((col <= col_start && col + width - 1 >= col_start) || (col <= col_end && col + width - 1 >= col_end) || (col >= col_start && col + width - 1 <= col_end)) { xassert(!would_have_breaked); struct sixel to_be_erased = *six; tll_remove(term->grid->sixel_images, it); sixel_overwrite(term, &to_be_erased, start, col, height, width, pix, opaque); sixel_erase(term, &to_be_erased); } else xassert(!collides); } else xassert(!collides); #if defined(_DEBUG) pixman_region32_fini(&intersection); pixman_region32_fini(&six_rect); #endif } #if defined(_DEBUG) pixman_region32_fini(&overwrite_rect); #endif } void sixel_overwrite_by_rectangle( struct terminal *term, int row, int col, int height, int width) { if (likely(tll_length(term->grid->sixel_images) == 0)) return; const int start = (term->grid->offset + row) & (term->grid->num_rows - 1); const int end = (start + height - 1) & (term->grid->num_rows - 1); const bool wraps = end < start; if (wraps) { int rows_to_wrap_around = term->grid->num_rows - start; xassert(height - rows_to_wrap_around > 0); _sixel_overwrite_by_rectangle(term, start, col, rows_to_wrap_around, width, NULL, NULL); _sixel_overwrite_by_rectangle(term, 0, col, height - rows_to_wrap_around, width, NULL, NULL); } else _sixel_overwrite_by_rectangle(term, start, col, height, width, NULL, NULL); term->bits_affecting_ascii_printer.sixels = tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); } /* Row numbers are relative to grid offset */ void sixel_overwrite_by_row(struct terminal *term, int _row, int col, int width) { xassert(col >= 0); xassert(_row >= 0); xassert(_row < term->rows); xassert(col >= 0); xassert(col < term->grid->num_cols); if (likely(tll_length(term->grid->sixel_images) == 0)) return; if (col + width > term->grid->num_cols) width = term->grid->num_cols - col; const int row = (term->grid->offset + _row) & (term->grid->num_rows - 1); const int scrollback_rel_row = grid_row_abs_to_sb(term->grid, term->rows, row); tll_foreach(term->grid->sixel_images, it) { struct sixel *six = &it->item; const int six_start = six->pos.row; const int six_end = (six_start + six->rows - 1) & (term->grid->num_rows - 1); /* We should never generate scrollback wrapping sixels */ xassert(six_end >= six_start); const int six_scrollback_rel_end = grid_row_abs_to_sb(term->grid, term->rows, six_end); if (six_scrollback_rel_end < scrollback_rel_row) { /* All remaining sixels are *before* "our" row */ break; } if (row >= six_start && row <= six_end) { const int col_start = six->pos.col; const int col_end = six->pos.col + six->cols - 1; if ((col <= col_start && col + width - 1 >= col_start) || (col <= col_end && col + width - 1 >= col_end) || (col >= col_start && col + width - 1 <= col_end)) { struct sixel to_be_erased = *six; tll_remove(term->grid->sixel_images, it); sixel_overwrite(term, &to_be_erased, row, col, 1, width, NULL, NULL); sixel_erase(term, &to_be_erased); } } } term->bits_affecting_ascii_printer.sixels = tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); } void sixel_overwrite_at_cursor(struct terminal *term, int width) { if (likely(tll_length(term->grid->sixel_images) == 0)) return; sixel_overwrite_by_row( term, term->grid->cursor.point.row, term->grid->cursor.point.col, width); } void sixel_cell_size_changed(struct terminal *term) { tll_foreach(term->normal.sixel_images, it) sixel_invalidate_cache(&it->item); tll_foreach(term->alt.sixel_images, it) sixel_invalidate_cache(&it->item); } void sixel_sync_cache(const struct terminal *term, struct sixel *six) { if (six->pix != NULL) { #if defined(_DEBUG) if (six->cell_width == term->cell_width && six->cell_height == term->cell_height) { xassert(six->pix == six->original.pix); xassert(six->width == six->original.width); xassert(six->height == six->original.height); xassert(six->scaled.data == NULL); xassert(six->scaled.pix == NULL); xassert(six->scaled.width < 0); xassert(six->scaled.height < 0); } else { xassert(six->pix == six->scaled.pix); xassert(six->width == six->scaled.width); xassert(six->height == six->scaled.height); xassert(six->scaled.data != NULL); xassert(six->scaled.pix != NULL); /* TODO: check ratio */ xassert(six->scaled.width >= 0); xassert(six->scaled.height >= 0); } #endif return; } /* Cache should be invalid */ xassert(six->scaled.data == NULL); xassert(six->scaled.pix == NULL); xassert(six->scaled.width < 0); xassert(six->scaled.height < 0); if (six->cell_width == term->cell_width && six->cell_height == term->cell_height) { six->pix = six->original.pix; six->width = six->original.width; six->height = six->original.height; } else { const double width_ratio = (double)term->cell_width / six->cell_width; const double height_ratio = (double)term->cell_height / six->cell_height; struct pixman_f_transform scale; pixman_f_transform_init_scale( &scale, 1. / width_ratio, 1. / height_ratio); struct pixman_transform _scale; pixman_transform_from_pixman_f_transform(&_scale, &scale); pixman_image_set_transform(six->original.pix, &_scale); pixman_image_set_filter(six->original.pix, PIXMAN_FILTER_BILINEAR, NULL, 0); int scaled_width = (double)six->original.width * width_ratio; int scaled_height = (double)six->original.height * height_ratio; int scaled_stride = scaled_width * sizeof(uint32_t); LOG_DBG("scaling sixel: %dx%d -> %dx%d", six->original.width, six->original.height, scaled_width, scaled_height); uint8_t *scaled_data = xmalloc(scaled_height * scaled_stride); pixman_image_t *scaled_pix = pixman_image_create_bits_no_clear( term->sixel.pixman_fmt, scaled_width, scaled_height, (uint32_t *)scaled_data, scaled_stride); pixman_image_composite32( PIXMAN_OP_SRC, six->original.pix, NULL, scaled_pix, 0, 0, 0, 0, 0, 0, scaled_width, scaled_height); pixman_image_set_transform(six->original.pix, NULL); six->scaled.data = scaled_data; six->scaled.pix = six->pix = scaled_pix; six->scaled.width = six->width = scaled_width; six->scaled.height = six->height = scaled_height; } } void sixel_reflow_grid(struct terminal *term, struct grid *grid) { /* Meh - the sixel functions we call use term->grid... */ struct grid *active_grid = term->grid; term->grid = grid; /* Need the "real" list to be empty from the beginning */ tll(struct sixel) copy = tll_init(); tll_foreach(grid->sixel_images, it) tll_push_back(copy, it->item); tll_free(grid->sixel_images); tll_rforeach(copy, it) { struct sixel *six = &it->item; int start = six->pos.row; int end = (start + six->rows - 1) & (grid->num_rows - 1); if (end < start) { /* Crosses scrollback wrap-around */ /* TODO: split image */ sixel_destroy(six); continue; } if (six->rows > grid->num_rows) { /* Image too large */ /* TODO: keep bottom part? */ sixel_destroy(six); continue; } /* Drop sixels that now cross the current scrollback end * border. This is similar to a sixel that have been * scrolled out */ /* TODO: should be possible to optimize this */ bool sixel_destroyed = false; int last_row = -1; for (int j = 0; j < six->rows; j++) { int row_no = grid_row_abs_to_sb( term->grid, term->rows, six->pos.row + j); if (last_row != -1 && last_row >= row_no) { sixel_destroy(six); sixel_destroyed = true; break; } last_row = row_no; } if (sixel_destroyed) { LOG_WARN("destroyed sixel that now crossed history"); continue; } /* Sixels that didn't overlap may now do so, which isn't * allowed of course */ _sixel_overwrite_by_rectangle( term, six->pos.row, six->pos.col, six->rows, six->cols, &it->item.original.pix, &it->item.opaque); if (it->item.original.data != pixman_image_get_data(it->item.original.pix)) { it->item.original.data = pixman_image_get_data(it->item.original.pix); it->item.original.width = pixman_image_get_width(it->item.original.pix); it->item.original.height = pixman_image_get_height(it->item.original.pix); it->item.cols = (it->item.original.width + it->item.cell_width - 1) / it->item.cell_width; it->item.rows = (it->item.original.height + it->item.cell_height - 1) / it->item.cell_height; sixel_invalidate_cache(&it->item); } sixel_insert(term, it->item); } tll_free(copy); term->grid = active_grid; } void sixel_reflow(struct terminal *term) { for (size_t i = 0; i < 2; i++) { struct grid *grid = i == 0 ? &term->normal : &term->alt; sixel_reflow_grid(term, grid); } } void sixel_unhook(struct terminal *term) { if (term->sixel.pos.row < term->sixel.image.height && term->sixel.pos.row + 6 * term->sixel.pan >= term->sixel.image.height) { /* * Handle case where image has had its size set by raster * attributes, and then one or more sixels were printed on the * last row of the RA area. * * In this case, the image height may not be a multiple of * 6*pan. But the printed sixels may still be outside the RA * area. In this case, using the size from the RA would * truncate the image. * * So, extend the image to a multiple of 6*pan. * * If this is a transparent image, the image may get trimmed * below (most likely back the size set by RA). */ term->sixel.image.height = term->sixel.image.alloc_height; } /* Strip trailing fully transparent rows, *unless* we *ended* with * a trailing GNL, in which case we do *not* want to strip all 6 * pixel rows */ if (term->sixel.pos.col > 0) { const int bits = sizeof(term->sixel.image.bottom_pixel) * 8; const int leading_zeroes = term->sixel.image.bottom_pixel == 0 ? bits : __builtin_clz(term->sixel.image.bottom_pixel); const int rows_to_trim = leading_zeroes + 6 - bits; LOG_DBG("bottom-pixel: 0x%02x, bits=%d, leading-zeroes=%d, " "rows-to-trim=%d*%d", term->sixel.image.bottom_pixel, bits, leading_zeroes, rows_to_trim, term->sixel.pan); /* * If the current graphical cursor position is at the last row * of the image, *and* the image is transparent (P2=1), trim * the entire image. * * If the image is not transparent, then we can't trim the RA * region (it is supposed to "erase", with the current * background color.) * * We *do* "trim" transparent rows from the graphical cursor * position, as this affects the positioning of the text * cursor. * * See https://raw.githubusercontent.com/hackerb9/vt340test/main/sixeltests/p2effect.sh */ if (term->sixel.pos.row + 6 * term->sixel.pan >= term->sixel.image.alloc_height) { LOG_DBG("trimming image"); const int trimmed_height = term->sixel.image.alloc_height - rows_to_trim * term->sixel.pan; if (term->sixel.transparent_bg) { /* Image is transparent - trim as much as possible */ term->sixel.image.height = trimmed_height; } else { /* Image is opaque. We can't trim anything "inside" the RA region */ if (trimmed_height > term->sixel.image.height) { /* There are non-empty pixels *outside* the RA region - trim up to that point */ term->sixel.image.height = trimmed_height; } } } else { LOG_DBG("only adjusting cursor position"); } term->sixel.pos.row += 6 * term->sixel.pan; term->sixel.pos.row -= rows_to_trim * term->sixel.pan; } int pixel_row_idx = 0; int pixel_rows_left = term->sixel.image.height; const int stride = term->sixel.image.width * sizeof(uint32_t); /* * When sixel scrolling is enabled (the default), sixels behave * pretty much like normal output; the sixel starts at the current * cursor position and the cursor is moved to a point after the * sixel. * * Furthermore, if the sixel reaches the bottom of the scrolling * region, the terminal content is scrolled. * * When scrolling is disabled, sixels always start at (0,0), the * cursor is not moved at all, and the terminal content never * scrolls. */ const bool do_scroll = term->sixel.scrolling; /* Number of rows we're allowed to use. * * When scrolling is enabled, we always allow the entire sixel to * be emitted. * * When disabled, only the number of screen rows may be used. */ int rows_avail = do_scroll ? (term->sixel.image.height + term->cell_height - 1) / term->cell_height : term->scroll_region.end; /* Initial sixel coordinates */ int start_row = do_scroll ? term->grid->cursor.point.row : 0; const int start_col = do_scroll ? term->grid->cursor.point.col : 0; /* Total number of rows needed by image */ const int rows_needed = (term->sixel.image.height + term->cell_height - 1) / term->cell_height; bool free_image_data = true; /* We do not allow sixels to cross the scrollback wrap-around, as * this makes intersection calculations much more complicated */ while (pixel_rows_left > 0 && rows_avail > 0 && rows_needed <= term->grid->num_rows) { const int cur_row = (term->grid->offset + start_row) & (term->grid->num_rows - 1); const int rows_left_until_wrap_around = term->grid->num_rows - cur_row; const int usable_rows = min(rows_avail, rows_left_until_wrap_around); const int pixel_rows_avail = usable_rows * term->cell_height; const int width = term->sixel.image.width; const int height = min(pixel_rows_left, pixel_rows_avail); uint32_t *img_data; if (pixel_row_idx == 0 && height == pixel_rows_left) { /* Entire image will be emitted as a single chunk - reuse * the source buffer */ img_data = term->sixel.image.data; free_image_data = false; } else { xassert(free_image_data); img_data = xmalloc(height * stride); memcpy( img_data, &((uint8_t *)term->sixel.image.data)[pixel_row_idx * stride], height * stride); } struct sixel image = { .pix = NULL, .width = -1, .height = -1, .rows = (height + term->cell_height - 1) / term->cell_height, .cols = (width + term->cell_width - 1) / term->cell_width, .pos = (struct coord){start_col, cur_row}, .opaque = !term->sixel.transparent_bg, .cell_width = term->cell_width, .cell_height = term->cell_height, .original = { .data = img_data, .pix = NULL, .width = width, .height = height, }, .scaled = { .data = NULL, .pix = NULL, .width = -1, .height = -1, }, }; xassert(image.rows <= term->grid->num_rows); xassert(image.pos.row + image.rows - 1 < term->grid->num_rows); LOG_DBG("generating %s %dx%d pixman image at %d-%d", image.opaque ? "opaque" : "transparent", image.original.width, image.original.height, image.pos.row, image.pos.row + image.rows); image.original.pix = pixman_image_create_bits_no_clear( term->sixel.pixman_fmt, image.original.width, image.original.height, img_data, stride); pixel_row_idx += height; pixel_rows_left -= height; rows_avail -= image.rows; if (do_scroll) { /* * Linefeeds - always one less than the number of rows * occupied by the image. * * Unless this is *not* the last chunk. In that case, * linefeed past the chunk, so that the next chunk * "starts" at a "new" row. */ const int linefeed_count = rows_avail == 0 ? max(0, image.rows - 1) : image.rows; xassert(rows_avail == 0 || image.original.height % term->cell_height == 0); for (size_t i = 0; i < linefeed_count; i++) term_linefeed(term); /* Position text cursor if this is the last image chunk */ if (rows_avail == 0) { int row = term->grid->cursor.point.row; /* * Position the text cursor based on the text row * touched by the last sixel */ const int pixel_rows = pixel_rows_left > 0 ? image.original.height : term->sixel.pos.row; const int term_rows = (pixel_rows + term->cell_height - 1) / term->cell_height; xassert(term_rows <= image.rows); row -= (image.rows - term_rows); term_cursor_to( term, max(0, row), (term->sixel.cursor_right_of_graphics ? min(image.pos.col + image.cols, term->cols - 1) : image.pos.col)); } term->sixel.pos.row -= image.original.height; } /* Dirty touched cells, and scroll terminal content if necessary */ for (size_t i = 0; i < image.rows; i++) { struct row *row = term->grid->rows[cur_row + i]; row->dirty = true; for (int col = image.pos.col; col < min(image.pos.col + image.cols, term->cols); col++) { row->cells[col].attrs.clean = 0; } } _sixel_overwrite_by_rectangle( term, image.pos.row, image.pos.col, image.rows, image.cols, &image.original.pix, &image.opaque); if (image.original.data != pixman_image_get_data(image.original.pix)) { image.original.data = pixman_image_get_data(image.original.pix); image.original.width = pixman_image_get_width(image.original.pix); image.original.height = pixman_image_get_height(image.original.pix); image.cols = (image.original.width + image.cell_width - 1) / image.cell_width; image.rows = (image.original.height + image.cell_height - 1) / image.cell_height; sixel_invalidate_cache(&image); } sixel_insert(term, image); if (do_scroll) start_row = term->grid->cursor.point.row; else start_row -= image.rows; } if (free_image_data) free(term->sixel.image.data); term->sixel.image.data = NULL; term->sixel.image.p = NULL; term->sixel.image.width = 0; term->sixel.image.height = 0; term->sixel.pos = (struct coord){0, 0}; free(term->sixel.private_palette); term->sixel.private_palette = NULL; LOG_DBG("you now have %zu sixels in current grid", tll_length(term->grid->sixel_images)); term->bits_affecting_ascii_printer.sixels = tll_length(term->grid->sixel_images) > 0; term_update_ascii_printer(term); render_refresh(term); } static void ALWAYS_INLINE inline memset_u32(uint32_t *data, uint32_t value, size_t count) { static_assert(sizeof(wchar_t) == 4, "wchar_t is not 4 bytes"); wmemset((wchar_t *)data, (wchar_t)value, count); } static void resize_horizontally(struct terminal *term, int new_width_mutable) { if (unlikely(new_width_mutable > term->sixel.max_width)) { LOG_WARN("maximum image dimensions exceeded, truncating"); new_width_mutable = term->sixel.max_width; } if (unlikely(term->sixel.image.width >= new_width_mutable)) return; const int sixel_row_height = 6 * term->sixel.pan; uint32_t *old_data = term->sixel.image.data; const int old_width = term->sixel.image.width; const int new_width = new_width_mutable; int height; if (unlikely(term->sixel.image.height == 0)) { /* Lazy initialize height on first printed sixel */ xassert(old_width == 0); term->sixel.image.height = height = sixel_row_height; term->sixel.image.alloc_height = sixel_row_height; } else height = term->sixel.image.height; LOG_DBG("resizing image horizontally: %dx(%d) -> %dx(%d)", term->sixel.image.width, term->sixel.image.height, new_width, height); int alloc_height = (height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; xassert(new_width >= old_width); xassert(new_width > 0); xassert(alloc_height > 0); /* Width (and thus stride) change - need to allocate a new buffer */ uint32_t *new_data = xmalloc(new_width * alloc_height * sizeof(uint32_t)); uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; /* Copy old rows, and initialize new columns to background color */ const uint32_t *end = &new_data[alloc_height * new_width]; for (uint32_t *n = new_data, *o = old_data; n < end; n += new_width, o += old_width) { memcpy(n, o, old_width * sizeof(uint32_t)); memset_u32(&n[old_width], bg, new_width - old_width); } free(old_data); term->sixel.image.data = new_data; term->sixel.image.width = new_width; const int ofs = term->sixel.pos.row * new_width + term->sixel.pos.col; term->sixel.image.p = &term->sixel.image.data[ofs]; } static bool resize_vertically(struct terminal *term, const int new_height) { LOG_DBG("resizing image vertically: (%d)x%d -> (%d)x%d", term->sixel.image.width, term->sixel.image.height, term->sixel.image.width, new_height); if (unlikely(new_height > term->sixel.max_height)) { LOG_WARN("maximum image dimensions reached"); return false; } uint32_t *old_data = term->sixel.image.data; const int width = term->sixel.image.width; const int old_height = term->sixel.image.height; const int sixel_row_height = 6 * term->sixel.pan; int alloc_height = (new_height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; xassert(new_height > 0); if (unlikely(width == 0)) { xassert(term->sixel.image.data == NULL); term->sixel.image.height = new_height; term->sixel.image.alloc_height = alloc_height; return true; } uint32_t *new_data = realloc( old_data, width * alloc_height * sizeof(uint32_t)); if (new_data == NULL) { LOG_ERRNO("failed to reallocate sixel image buffer"); return false; } const uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; memset_u32(&new_data[old_height * width], bg, (alloc_height - old_height) * width); term->sixel.image.height = new_height; term->sixel.image.alloc_height = alloc_height; const int ofs = term->sixel.pos.row * term->sixel.image.width + term->sixel.pos.col; term->sixel.image.data = new_data; term->sixel.image.p = &term->sixel.image.data[ofs]; return true; } static bool resize(struct terminal *term, int new_width_mutable, int new_height_mutable) { LOG_DBG("resizing image: %dx%d -> %dx%d", term->sixel.image.width, term->sixel.image.height, new_width_mutable, new_height_mutable); if (unlikely(new_width_mutable > term->sixel.max_width)) { LOG_WARN("maximum image width exceeded, truncating"); new_width_mutable = term->sixel.max_width; } if (unlikely(new_height_mutable > term->sixel.max_height)) { LOG_WARN("maximum image height exceeded, truncating"); new_height_mutable = term->sixel.max_height; } uint32_t *old_data = term->sixel.image.data; const int old_width = term->sixel.image.width; const int old_height = term->sixel.image.height; const int new_width = new_width_mutable; const int new_height = new_height_mutable; if (unlikely(old_width == new_width && old_height == new_height)) return true; const int sixel_row_height = 6 * term->sixel.pan; const int alloc_new_height = (new_height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; xassert(alloc_new_height >= new_height); xassert(alloc_new_height - new_height < sixel_row_height); uint32_t *new_data = NULL; const uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; /* * If the image is resized horizontally, or if it's opaque, we * need to explicitly initialize the "new" pixels. * * When the image is *not* resized horizontally, we simply do a * realloc(). In this case, there's no need to manually copy the * old pixels. We do however need to initialize the new pixels * since realloc() returns uninitialized memory. * * When the image *is* resized horizontally, we need to allocate * new memory (when the width changes, the stride changes, and * thus we cannot simply realloc()) * * If the default background is transparent, the new pixels need * to be initialized to 0x0. We do this by using calloc(). * * If the default background is opaque, then we need to manually * initialize the new pixels. */ const bool initialize_bg = !term->sixel.transparent_bg || new_width == old_width; if (new_width == old_width) { /* Width (and thus stride) is the same, so we can simply * re-alloc the existing buffer */ new_data = realloc(old_data, new_width * alloc_new_height * sizeof(uint32_t)); if (new_data == NULL) { LOG_ERRNO("failed to reallocate sixel image buffer"); return false; } xassert(new_height > old_height); } else { /* Width (and thus stride) change - need to allocate a new buffer */ xassert(new_width > old_width); const size_t pixels = new_width * alloc_new_height; new_data = !initialize_bg ? xcalloc(pixels, sizeof(uint32_t)) : xmalloc(pixels * sizeof(uint32_t)); /* Copy old rows, and initialize new columns to background color */ const int row_copy_count = min(old_height, alloc_new_height); const uint32_t *end = &new_data[row_copy_count * new_width]; for (uint32_t *n = new_data, *o = old_data; n < end; n += new_width, o += old_width) { memcpy(n, o, old_width * sizeof(uint32_t)); memset_u32(&n[old_width], bg, new_width - old_width); } free(old_data); } if (initialize_bg) { memset_u32(&new_data[old_height * new_width], bg, (alloc_new_height - old_height) * new_width); } xassert(new_data != NULL); term->sixel.image.data = new_data; term->sixel.image.width = new_width; term->sixel.image.height = new_height; term->sixel.image.alloc_height = alloc_new_height; term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * new_width + term->sixel.pos.col]; return true; } static void sixel_add_generic(struct terminal *term, uint32_t *data, int stride, uint32_t color, uint8_t sixel) { const int pan = term->sixel.pan; for (int i = 0; i < 6; i++, sixel >>= 1) { if (sixel & 1) { for (int r = 0; r < pan; r++, data += stride) *data = color; } else data += stride * pan; } xassert(sixel == 0); } static void ALWAYS_INLINE inline sixel_add_ar_11(struct terminal *term, uint32_t *data, int stride, uint32_t color, uint8_t sixel) { xassert(term->sixel.pan == 1); if (sixel & 0x01) *data = color; data += stride; if (sixel & 0x02) *data = color; data += stride; if (sixel & 0x04) *data = color; data += stride; if (sixel & 0x08) *data = color; data += stride; if (sixel & 0x10) *data = color; data += stride; if (sixel & 0x20) *data = color; } static void sixel_add_many_generic(struct terminal *term, uint8_t c, unsigned count) { int col = term->sixel.pos.col; int width = term->sixel.image.width; count *= term->sixel.pad; if (unlikely(col + count - 1 >= width)) { resize_horizontally(term, col + count); width = term->sixel.image.width; count = min(count, max(width - col, 0)); if (unlikely(count == 0)) return; } uint32_t color = term->sixel.color; uint32_t *data = term->sixel.image.p; uint32_t *end = data + count; term->sixel.pos.col = col + count; term->sixel.image.p = end; term->sixel.image.bottom_pixel |= c; for (; data < end; data++) sixel_add_generic(term, data, width, color, c); } static void ALWAYS_INLINE inline sixel_add_one_ar_11(struct terminal *term, uint8_t c) { xassert(term->sixel.pan == 1); xassert(term->sixel.pad == 1); int col = term->sixel.pos.col; int width = term->sixel.image.width; if (unlikely(col >= width)) { resize_horizontally(term, col + count); width = term->sixel.image.width; count = min(count, max(width - col, 0)); if (unlikely(count == 0)) return; } uint32_t *data = term->sixel.image.p; term->sixel.pos.col += 1; term->sixel.image.p += 1; term->sixel.image.bottom_pixel |= c; sixel_add_ar_11(term, data, width, term->sixel.color, c); } static void sixel_add_many_ar_11(struct terminal *term, uint8_t c, unsigned count) { xassert(term->sixel.pan == 1); xassert(term->sixel.pad == 1); int col = term->sixel.pos.col; int width = term->sixel.image.width; if (unlikely(col + count - 1 >= width)) { resize_horizontally(term, col + count); width = term->sixel.image.width; count = min(count, max(width - col, 0)); if (unlikely(count == 0)) return; } uint32_t color = term->sixel.color; uint32_t *data = term->sixel.image.p; uint32_t *end = data + count; term->sixel.pos.col += count; term->sixel.image.p = end; term->sixel.image.bottom_pixel |= c; for (; data < end; data++) sixel_add_ar_11(term, data, width, color, c); } IGNORE_WARNING("-Wpedantic") static void decsixel_generic(struct terminal *term, uint8_t c) { switch (c) { case '"': term->sixel.state = SIXEL_DECGRA; term->sixel.param = 0; term->sixel.param_idx = 0; break; case '!': term->sixel.state = SIXEL_DECGRI; term->sixel.param = 0; term->sixel.param_idx = 0; term->sixel.repeat_count = 1; break; case '#': term->sixel.state = SIXEL_DECGCI; term->sixel.color_idx = 0; term->sixel.param = 0; term->sixel.param_idx = 0; break; case '$': if (likely(term->sixel.pos.col <= term->sixel.max_width)) { /* * We set, and keep, 'col' outside the image boundary when * we've reached the maximum image height, to avoid also * having to check the row vs image height in the common * path in sixel_add(). */ term->sixel.pos.col = 0; term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * term->sixel.image.width]; } break; case '-': /* GNL - Graphical New Line */ term->sixel.pos.row += 6 * term->sixel.pan; term->sixel.pos.col = 0; term->sixel.image.bottom_pixel = 0; term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * term->sixel.image.width]; if (term->sixel.pos.row >= term->sixel.image.alloc_height) { if (!resize_vertically(term, term->sixel.pos.row + 6 * term->sixel.pan)) term->sixel.pos.col = term->sixel.max_width + 1 * term->sixel.pad; } break; case '?' ... '~': sixel_add_many_generic(term, c - 63, 1); break; case ' ': case '\n': case '\r': break; default: LOG_WARN("invalid sixel character: '%c' at idx=%zu", c, count); break; } } UNIGNORE_WARNINGS static void decsixel_ar_11(struct terminal *term, uint8_t c) { if (likely(c >= '?' && c <= '~')) sixel_add_one_ar_11(term, c - 63); else decsixel_generic(term, c); } static void decgra(struct terminal *term, uint8_t c) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': term->sixel.param *= 10; term->sixel.param += c - '0'; break; case ';': if (term->sixel.param_idx < ALEN(term->sixel.params)) term->sixel.params[term->sixel.param_idx++] = term->sixel.param; term->sixel.param = 0; break; default: { if (term->sixel.param_idx < ALEN(term->sixel.params)) term->sixel.params[term->sixel.param_idx++] = term->sixel.param; int nparams = term->sixel.param_idx; unsigned pan = nparams > 0 ? term->sixel.params[0] : 0; unsigned pad = nparams > 1 ? term->sixel.params[1] : 0; unsigned ph = nparams > 2 ? term->sixel.params[2] : 0; unsigned pv = nparams > 3 ? term->sixel.params[3] : 0; pan = pan > 0 ? pan : 1; pad = pad > 0 ? pad : 1; if (likely(term->sixel.image.width == 0 && term->sixel.image.height == 0)) { term->sixel.pan = pan; term->sixel.pad = pad; } else { /* * Unsure what the VT340 does... * * We currently do *not* handle changing pan/pad in the * middle of a sixel, since that means resizing/stretching * the existing image. * * I'm *guessing* the VT340 simply changes the aspect * ratio of all subsequent sixels. But, given the design * of our implementation (the entire sixel is written to a * single pixman image), we can't easily do that. */ LOG_WARN("sixel: unsupported: pan/pad changed after printing sixels"); pan = term->sixel.pan; pad = term->sixel.pad; } pv *= pan; ph *= pad; LOG_DBG("pan=%u, pad=%u (aspect ratio = %d:%d), size=%ux%u", pan, pad, pan, pad, ph, pv); /* * RA really only acts as a rectangular erase - it fills the * specified area with the sixel background color[^1]. Nothing * else. It does *not* affect cursor positioning. * * This means that if the emitted sixel is *smaller* than the * RA, the text cursor will be placed "inside" the RA area. * * This means it would be more correct to view the RA area as * a *separate* sixel image, that is then overlaid with the * actual sixel. * * Still, RA _is_ a hint - the final image is _likely_ going * to be this large. And, treating RA as a separate image * prevents us from pre-allocating the final sixel image. * * So we don't. We use the RA as a hint, and pre-allocates the * backing image buffer. * * [^1]: i.e. it's a NOP if the sixel is transparent */ if (ph >= term->sixel.image.height && pv >= term->sixel.image.width && ph <= term->sixel.max_height && pv <= term->sixel.max_width) { /* * TODO: always resize to a multiple of 6*pan? * * We're effectively doing that already, except * sixel.image.height is set to ph, instead of the * allocated height (which is always a multiple of 6*pan). * * If the user wants to emit a sixel that isn't a multiple * of 6 pixels, the bottom sixel rows should all be empty, * and (assuming a transparent sixel), trimmed when the * final image is generated. */ resize(term, ph, pv); } term->sixel.state = SIXEL_DECSIXEL; /* Update DCS put handler, since pan/pad may have changed */ term->vt.dcs.put_handler = pan == 1 && pad == 1 ? &sixel_put_ar_11 : &sixel_put_generic; if (likely(pan == 1 && pad == 1)) decsixel_ar_11(term, c); else decsixel_generic(term, c); break; } } } IGNORE_WARNING("-Wpedantic") static void decgri_generic(struct terminal *term, uint8_t c) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': { unsigned param = term->sixel.param; param *= 10; param += c - '0'; term->sixel.repeat_count = term->sixel.param = param; break; } case '?' ... '~': { unsigned count = term->sixel.repeat_count; if (unlikely(count == 0)) { count = 1; } sixel_add_many_generic(term, c - 63, count); term->sixel.state = SIXEL_DECSIXEL; break; } default: term->sixel.state = SIXEL_DECSIXEL; term->vt.dcs.put_handler(term, c); break; } } UNIGNORE_WARNINGS static void decgri_ar_11(struct terminal *term, uint8_t c) { if (likely(c >= '?' && c <= '~')) { unsigned count = term->sixel.repeat_count; if (unlikely(count == 0)) { count = 1; } sixel_add_many_ar_11(term, c - 63, count); term->sixel.state = SIXEL_DECSIXEL; } else decgri_generic(term, c); } static void decgci(struct terminal *term, uint8_t c) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': term->sixel.param *= 10; term->sixel.param += c - '0'; break; case ';': if (term->sixel.param_idx < ALEN(term->sixel.params)) term->sixel.params[term->sixel.param_idx++] = term->sixel.param; term->sixel.param = 0; break; default: { if (term->sixel.param_idx < ALEN(term->sixel.params)) term->sixel.params[term->sixel.param_idx++] = term->sixel.param; int nparams = term->sixel.param_idx; if (nparams > 0) term->sixel.color_idx = min(term->sixel.params[0], term->sixel.palette_size - 1); if (nparams > 4) { unsigned format = term->sixel.params[1]; int c1 = term->sixel.params[2]; int c2 = term->sixel.params[3]; int c3 = term->sixel.params[4]; switch (format) { case 1: { /* HLS */ int hue = min(c1, 360); int lum = min(c2, 100); int sat = min(c3, 100); /* * Sixel's HLS use the following primary color hues: * blue: 0° * red: 120° * green: 240° * * While "standard" HSL uses: * red: 0° * green: 120° * blue: 240° */ hue = (hue + 240) % 360; uint32_t rgb = hsl_to_rgb(hue, sat, lum); LOG_DBG("setting palette #%d = HLS %hhu/%hhu/%hhu (0x%06x)", term->sixel.color_idx, hue, lum, sat, rgb); term->sixel.palette[term->sixel.color_idx] = 0xffu << 24 | rgb; break; } case 2: { /* RGB */ uint16_t r = 255 * min(c1, 100) / 100; uint16_t g = 255 * min(c2, 100) / 100; uint16_t b = 255 * min(c3, 100) / 100; LOG_DBG("setting palette #%d = RGB %hu/%hu/%hu", term->sixel.color_idx, r, g, b); term->sixel.palette[term->sixel.color_idx] = color_decode_srgb(term, r, g, b); break; } } } else term->sixel.color = term->sixel.palette[term->sixel.color_idx]; term->sixel.state = SIXEL_DECSIXEL; if (likely(term->sixel.pan == 1 && term->sixel.pad == 1)) decsixel_ar_11(term, c); else decsixel_generic(term, c); break; } } } static void sixel_put_generic(struct terminal *term, uint8_t c) { switch (term->sixel.state) { case SIXEL_DECSIXEL: decsixel_generic(term, c); break; case SIXEL_DECGRA: decgra(term, c); break; case SIXEL_DECGRI: decgri_generic(term, c); break; case SIXEL_DECGCI: decgci(term, c); break; } count++; } static void sixel_put_ar_11(struct terminal *term, uint8_t c) { switch (term->sixel.state) { case SIXEL_DECSIXEL: decsixel_ar_11(term, c); break; case SIXEL_DECGRA: decgra(term, c); break; case SIXEL_DECGRI: decgri_ar_11(term, c); break; case SIXEL_DECGCI: decgci(term, c); break; } count++; } void sixel_colors_report_current(struct terminal *term) { char reply[24]; size_t n = xsnprintf(reply, sizeof(reply), "\033[?1;0;%uS", term->sixel.palette_size); term_to_slave(term, reply, n); LOG_DBG("query response for current color count: %u", term->sixel.palette_size); } void sixel_colors_reset(struct terminal *term) { LOG_DBG("sixel palette size reset to %u", SIXEL_MAX_COLORS); free(term->sixel.palette); term->sixel.palette = NULL; term->sixel.palette_size = SIXEL_MAX_COLORS; sixel_colors_report_current(term); } void sixel_colors_set(struct terminal *term, unsigned count) { unsigned new_palette_size = min(max(2, count), SIXEL_MAX_COLORS); LOG_DBG("sixel palette size set to %u", new_palette_size); free(term->sixel.private_palette); free(term->sixel.shared_palette); term->sixel.private_palette = NULL; term->sixel.shared_palette = NULL; term->sixel.palette_size = new_palette_size; sixel_colors_report_current(term); } void sixel_colors_report_max(struct terminal *term) { char reply[24]; size_t n = xsnprintf(reply, sizeof(reply), "\033[?1;0;%uS", SIXEL_MAX_COLORS); term_to_slave(term, reply, n); LOG_DBG("query response for max color count: %u", SIXEL_MAX_COLORS); } void sixel_geometry_report_current(struct terminal *term) { char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\033[?2;0;%u;%uS", min(term->cols * term->cell_width, term->sixel.max_width), min(term->rows * term->cell_height, term->sixel.max_height)); term_to_slave(term, reply, n); LOG_DBG("query response for current sixel geometry: %ux%u", term->sixel.max_width, term->sixel.max_height); } void sixel_geometry_reset(struct terminal *term) { LOG_DBG("sixel geometry reset to %ux%u", SIXEL_MAX_WIDTH, SIXEL_MAX_HEIGHT); term->sixel.max_width = SIXEL_MAX_WIDTH; term->sixel.max_height = SIXEL_MAX_HEIGHT; sixel_geometry_report_current(term); } void sixel_geometry_set(struct terminal *term, unsigned width, unsigned height) { LOG_DBG("sixel geometry set to %ux%u", width, height); term->sixel.max_width = width; term->sixel.max_height = height; sixel_geometry_report_current(term); } void sixel_geometry_report_max(struct terminal *term) { unsigned max_width = term->sixel.max_width; unsigned max_height = term->sixel.max_height; char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\033[?2;0;%u;%uS", max_width, max_height); term_to_slave(term, reply, n); LOG_DBG("query response for max sixel geometry: %ux%u", max_width, max_height); } foot-1.21.0/sixel.h000066400000000000000000000034531476600145200140510ustar00rootroot00000000000000#pragma once #include "terminal.h" #define SIXEL_MAX_COLORS 1024u #define SIXEL_MAX_WIDTH 10000u #define SIXEL_MAX_HEIGHT 10000u typedef void (*sixel_put)(struct terminal *term, uint8_t c); void sixel_fini(struct terminal *term); sixel_put sixel_init(struct terminal *term, int p1, int p2, int p3); void sixel_unhook(struct terminal *term); void sixel_destroy(struct sixel *sixel); void sixel_destroy_all(struct terminal *term); void sixel_scroll_up(struct terminal *term, int rows); void sixel_scroll_down(struct terminal *term, int rows); void sixel_cell_size_changed(struct terminal *term); void sixel_sync_cache(const struct terminal *term, struct sixel *sixel); void sixel_reflow_grid(struct terminal *term, struct grid *grid); /* Shortcut for sixel_reflow_grid(normal) + sixel_reflow_grid(alt) */ void sixel_reflow(struct terminal *term); /* * Remove sixel data from the specified location. Used when printing * or erasing characters, and when emitting new sixel images, to * remove sixel data that would otherwise be rendered on-top. * * Row numbers are relative to the current grid offset */ void sixel_overwrite_by_rectangle( struct terminal *term, int row, int col, int height, int width); void sixel_overwrite_by_row(struct terminal *term, int row, int col, int width); void sixel_overwrite_at_cursor(struct terminal *term, int width); void sixel_colors_report_current(struct terminal *term); void sixel_colors_reset(struct terminal *term); void sixel_colors_set(struct terminal *term, unsigned count); void sixel_colors_report_max(struct terminal *term); void sixel_geometry_report_current(struct terminal *term); void sixel_geometry_reset(struct terminal *term); void sixel_geometry_set(struct terminal *term, unsigned width, unsigned height); void sixel_geometry_report_max(struct terminal *term); foot-1.21.0/slave.c000066400000000000000000000327141476600145200140340ustar00rootroot00000000000000#include "slave.h" #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "slave" #define LOG_ENABLE_DBG 0 #include "log.h" #include "debug.h" #include "macros.h" #include "tokenize.h" #include "util.h" #include "xmalloc.h" extern char **environ; struct environ { size_t count; char **envp; }; #if !defined(EXECVPE) static char * find_file_in_path(const char *file) { if (strchr(file, '/') != NULL) return xstrdup(file); const char *env_path = getenv("PATH"); char *path_list = NULL; if (env_path != NULL && env_path[0] != '\0') path_list = xstrdup(env_path); else { size_t sc_path_len = confstr(_CS_PATH, NULL, 0); if (sc_path_len > 0) { path_list = xmalloc(sc_path_len); confstr(_CS_PATH, path_list, sc_path_len); } else return xstrdup(file); } for (const char *path = strtok(path_list, ":"); path != NULL; path = strtok(NULL, ":")) { char *full = xstrjoin3(path, "/", file); if (access(full, F_OK) == 0) { free(path_list); return full; } free(full); } free(path_list); return xstrdup(file); } static int foot_execvpe(const char *file, char *const argv[], char *const envp[]) { char *path = find_file_in_path(file); int ret = execve(path, argv, envp); /* * Getting here is an error */ free(path); return ret; } #else /* EXECVPE */ #define foot_execvpe(file, argv, envp) execvpe(file, argv, envp) #endif /* EXECVPE */ static bool is_valid_shell(const char *shell) { FILE *f = fopen("/etc/shells", "r"); if (f == NULL) goto err; char *_line = NULL; size_t count = 0; while (true) { errno = 0; ssize_t ret = getline(&_line, &count, f); if (ret < 0) { free(_line); break; } char *line = _line; { while (isspace(*line)) line++; if (line[0] != '\0') { char *end = line + strlen(line) - 1; while (isspace(*end)) end--; *(end + 1) = '\0'; } } if (line[0] == '#') continue; if (streq(line, shell)) { fclose(f); return true; } } err: if (f != NULL) fclose(f); return false; } enum user_notification_ret_t {UN_OK, UN_NO_MORE, UN_FAIL}; static enum user_notification_ret_t emit_one_notification(int fd, const struct user_notification *notif) { const char *prefix = NULL; const char *postfix = "\033[m\n"; switch (notif->kind) { case USER_NOTIFICATION_DEPRECATED: prefix = "\033[33;1mdeprecated\033[39;22m: "; break; case USER_NOTIFICATION_WARNING: prefix = "\033[33;1mwarning\033[39;22m: "; break; case USER_NOTIFICATION_ERROR: prefix = "\033[31;1merror\033[39;22m: "; break; } xassert(prefix != NULL); if (write(fd, prefix, strlen(prefix)) < 0 || write(fd, "foot: ", 6) < 0 || write(fd, notif->text, strlen(notif->text)) < 0 || write(fd, postfix, strlen(postfix)) < 0) { /* * The main process is blocking and waiting for us to close * the error pipe. Thus, pts data will *not* be processed * until we've exec:d. This means we cannot write anymore once * the kernel buffer is full. Don't treat this as a fatal * error. */ if (errno == EWOULDBLOCK || errno == EAGAIN) return UN_NO_MORE; else { LOG_ERRNO("failed to write user-notification"); return UN_FAIL; } } return UN_OK; } static bool emit_notifications_of_kind(int fd, const user_notifications_t *notifications, enum user_notification_kind kind) { tll_foreach(*notifications, it) { if (it->item.kind == kind) { switch (emit_one_notification(fd, &it->item)) { case UN_OK: break; case UN_NO_MORE: return true; case UN_FAIL: return false; } } } return true; } static bool emit_notifications(int fd, const user_notifications_t *notifications) { return emit_notifications_of_kind(fd, notifications, USER_NOTIFICATION_ERROR) && emit_notifications_of_kind(fd, notifications, USER_NOTIFICATION_WARNING) && emit_notifications_of_kind(fd, notifications, USER_NOTIFICATION_DEPRECATED); } static noreturn void slave_exec(int ptmx, char *argv[], char *const envp[], int err_fd, bool login_shell, const user_notifications_t *notifications) { int pts = -1; const char *pts_name = ptsname(ptmx); if (grantpt(ptmx) == -1) { LOG_ERRNO("failed to grantpt()"); goto err; } if (unlockpt(ptmx) == -1) { LOG_ERRNO("failed to unlockpt()"); goto err; } close(ptmx); ptmx = -1; if (setsid() == -1) { LOG_ERRNO("failed to setsid()"); goto err; } pts = open(pts_name, O_RDWR); if (pts == -1) { LOG_ERRNO("failed to open pseudo terminal slave device"); goto err; } if (ioctl(pts, TIOCSCTTY, 0) < 0) { LOG_ERRNO("failed to configure controlling terminal"); goto err; } #ifdef IUTF8 { struct termios flags; if (tcgetattr(pts, &flags) < 0) { LOG_ERRNO("failed to get terminal attributes"); goto err; } flags.c_iflag |= IUTF8; if (tcsetattr(pts, TCSANOW, &flags) < 0) { LOG_ERRNO("failed to set IUTF8 terminal attribute"); goto err; } } #endif if (tll_length(*notifications) > 0) { int flags = fcntl(pts, F_GETFL); if (flags < 0) goto err; if (fcntl(pts, F_SETFL, flags | O_NONBLOCK) < 0) goto err; if (!emit_notifications(pts, notifications)) goto err; fcntl(pts, F_SETFL, flags); } if (dup2(pts, STDIN_FILENO) == -1 || dup2(pts, STDOUT_FILENO) == -1 || dup2(pts, STDERR_FILENO) == -1) { LOG_ERRNO("failed to dup stdin/stdout/stderr"); goto err; } close(pts); pts = -1; const char *file; if (login_shell) { file = xstrdup(argv[0]); char *arg0 = xmalloc(strlen(argv[0]) + 1 + 1); arg0[0] = '-'; arg0[1] = '\0'; strcat(arg0, argv[0]); argv[0] = arg0; } else file = argv[0]; foot_execvpe(file, argv, envp); err: (void)!write(err_fd, &errno, sizeof(errno)); if (pts != -1) close(pts); if (ptmx != -1) close(ptmx); close(err_fd); _exit(errno); } static bool env_matches_var_name(const char *e, const char *name) { const size_t e_len = strlen(e); const size_t name_len = strlen(name); if (e_len <= name_len) return false; if (memcmp(e, name, name_len) != 0) return false; if (e[name_len] != '=') return false; return true; } static void add_to_env(struct environ *env, const char *name, const char *value) { if (env->envp == NULL) setenv(name, value, 1); else { char *e = xstrjoin3(name, "=", value); /* Search for existing variable. If found, replace it with the new value */ for (size_t i = 0; i < env->count; i++) { if (env_matches_var_name(env->envp[i], name)) { free(env->envp[i]); env->envp[i] = e; return; } } /* If the variable does not already exist, add it */ env->envp = xrealloc(env->envp, (env->count + 2) * sizeof(env->envp[0])); env->envp[env->count++] = e; env->envp[env->count] = NULL; } } static void del_from_env(struct environ *env, const char *name) { if (env->envp == NULL) unsetenv(name); else { for (size_t i = 0; i < env->count; i++) { if (env_matches_var_name(env->envp[i], name)) { free(env->envp[i]); memmove(&env->envp[i], &env->envp[i + 1], (env->count - i) * sizeof(env->envp[0])); env->count--; xassert(env->envp[env->count] == NULL); break; } } } } pid_t slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, const char *const *envp, const env_var_list_t *extra_env_vars, const char *term_env, const char *conf_shell, bool login_shell, const user_notifications_t *notifications) { int fork_pipe[2]; if (pipe2(fork_pipe, O_CLOEXEC) < 0) { LOG_ERRNO("failed to create pipe"); return -1; } pid_t pid = fork(); switch (pid) { case -1: LOG_ERRNO("failed to fork"); close(fork_pipe[0]); close(fork_pipe[1]); return -1; case 0: /* Child */ close(fork_pipe[0]); /* Close read end */ if (chdir(cwd) < 0) { const int errno_copy = errno; LOG_ERRNO("failed to change working directory to %s", cwd); (void)!write(fork_pipe[1], &errno_copy, sizeof(errno_copy)); _exit(errno_copy); } /* Restore signal mask, and SIG_IGN'd signals */ struct sigaction dfl = {.sa_handler = SIG_DFL}; sigemptyset(&dfl.sa_mask); sigset_t mask; sigemptyset(&mask); if (sigprocmask(SIG_SETMASK, &mask, NULL) < 0 || sigaction(SIGHUP, &dfl, NULL) < 0 || sigaction(SIGPIPE, &dfl, NULL) < 0) { const int errno_copy = errno; LOG_ERRNO_P(errno, "failed to restore signals"); (void)!write(fork_pipe[1], &errno_copy, sizeof(errno_copy)); _exit(errno_copy); } /* Create a mutable copy of the environment */ struct environ custom_env = {0}; if (envp != NULL) { for (const char *const *e = envp; *e != NULL; e++) custom_env.count++; custom_env.envp = xcalloc( custom_env.count + 1, sizeof(custom_env.envp[0])); size_t i = 0; for (const char *const *e = envp; *e != NULL; e++, i++) custom_env.envp[i] = xstrdup(*e); xassert(custom_env.envp[custom_env.count] == NULL); } add_to_env(&custom_env, "TERM", term_env); add_to_env(&custom_env, "COLORTERM", "truecolor"); add_to_env(&custom_env, "PWD", cwd); del_from_env(&custom_env, "TERM_PROGRAM"); del_from_env(&custom_env, "TERM_PROGRAM_VERSION"); #if defined(FOOT_TERMINFO_PATH) add_to_env(&custom_env, "TERMINFO", FOOT_TERMINFO_PATH); #endif if (extra_env_vars != NULL) { tll_foreach(*extra_env_vars, it) { const char *name = it->item.name; const char *value = it->item.value; if (strlen(value) == 0) del_from_env(&custom_env, name); else add_to_env(&custom_env, name, value); } } char **_shell_argv = NULL; char **shell_argv = NULL; if (argc == 0) { if (!tokenize_cmdline(conf_shell, &_shell_argv)) { (void)!write(fork_pipe[1], &errno, sizeof(errno)); _exit(0); } shell_argv = _shell_argv; } else { size_t count = 0; for (; argv[count] != NULL; count++) ; shell_argv = xmalloc((count + 1) * sizeof(shell_argv[0])); for (size_t i = 0; i < count; i++) shell_argv[i] = argv[i]; shell_argv[count] = NULL; } if (is_valid_shell(shell_argv[0])) add_to_env(&custom_env, "SHELL", shell_argv[0]); slave_exec(ptmx, shell_argv, custom_env.envp != NULL ? custom_env.envp : environ, fork_pipe[1], login_shell, notifications); BUG("Unexpected return from slave_exec()"); break; default: { /* * Don't stay in CWD, since it may be an ephemeral path. For * example, it may be a mount point of, say, a thumb drive. Us * keeping it open will prevent the user from unmounting it. */ (void)!!chdir("/"); close(fork_pipe[1]); /* Close write end */ LOG_DBG("slave has PID %d", pid); int errno_copy; static_assert(sizeof(errno) == sizeof(errno_copy), "errno size mismatch"); ssize_t ret = read(fork_pipe[0], &errno_copy, sizeof(errno_copy)); close(fork_pipe[0]); if (ret < 0) { LOG_ERRNO("failed to read from pipe"); return -1; } else if (ret == sizeof(errno_copy)) { LOG_ERRNO_P( errno_copy, "%s: failed to execute", argc == 0 ? conf_shell : argv[0]); return -1; } else LOG_DBG("%s: successfully started", conf_shell); int fd_flags; if ((fd_flags = fcntl(ptmx, F_GETFD)) < 0 || fcntl(ptmx, F_SETFD, fd_flags | FD_CLOEXEC) < 0) { LOG_ERRNO("failed to set FD_CLOEXEC on ptmx"); return -1; } break; } } return pid; } foot-1.21.0/slave.h000066400000000000000000000005651476600145200140400ustar00rootroot00000000000000#pragma once #include #include #include "config.h" #include "user-notification.h" pid_t slave_spawn( int ptmx, int argc, const char *cwd, char *const *argv, const char *const *envp, const env_var_list_t *extra_env_vars, const char *term_env, const char *conf_shell, bool login_shell, const user_notifications_t *notifications); foot-1.21.0/spawn.c000066400000000000000000000135061476600145200140500ustar00rootroot00000000000000#include "spawn.h" #include #include #include #include #include #include #include #define LOG_MODULE "spawn" #define LOG_ENABLE_DBG 0 #include "log.h" #include "debug.h" #include "xmalloc.h" pid_t spawn(struct reaper *reaper, const char *cwd, char *const argv[], int stdin_fd, int stdout_fd, int stderr_fd, reaper_cb cb, void *cb_data, const char *xdg_activation_token) { int pipe_fds[2] = {-1, -1}; if (pipe2(pipe_fds, O_CLOEXEC) < 0) { LOG_ERRNO("failed to create pipe"); goto err; } pid_t pid = fork(); if (pid < 0) { LOG_ERRNO("failed to fork"); goto err; } if (pid == 0) { /* Child */ close(pipe_fds[0]); if (setsid() < 0) goto child_err; /* Clear signal mask */ sigset_t mask; sigemptyset(&mask); if (sigprocmask(SIG_SETMASK, &mask, NULL) < 0) goto child_err; /* Restore ignored (SIG_IGN) signals */ struct sigaction dfl = {.sa_handler = SIG_DFL}; sigemptyset(&dfl.sa_mask); if (sigaction(SIGHUP, &dfl, NULL) < 0 || sigaction(SIGPIPE, &dfl, NULL) < 0) { goto child_err; } if (cwd != NULL) { setenv("PWD", cwd, 1); if (chdir(cwd) < 0) { LOG_WARN("failed to change working directory to %s: %s", cwd, strerror(errno)); } } if (xdg_activation_token != NULL) { setenv("XDG_ACTIVATION_TOKEN", xdg_activation_token, 1); if (getenv("DISPLAY") != NULL) setenv("DESKTOP_STARTUP_ID", xdg_activation_token, 1); } bool close_stderr = stderr_fd >= 0; bool close_stdout = stdout_fd >= 0 && stdout_fd != stderr_fd; bool close_stdin = stdin_fd >= 0 && stdin_fd != stdout_fd && stdin_fd != stderr_fd; if ((stdin_fd >= 0 && (dup2(stdin_fd, STDIN_FILENO) < 0 || (close_stdin && close(stdin_fd) < 0))) || (stdout_fd >= 0 && (dup2(stdout_fd, STDOUT_FILENO) < 0 || (close_stdout && close(stdout_fd) < 0))) || (stderr_fd >= 0 && (dup2(stderr_fd, STDERR_FILENO) < 0 || (close_stderr && close(stderr_fd) < 0))) || execvp(argv[0], argv) < 0) { goto child_err; } xassert(false); _exit(errno); child_err: ; const int errno_copy = errno; (void)!write(pipe_fds[1], &errno_copy, sizeof(errno_copy)); _exit(errno_copy); } /* Parent */ close(pipe_fds[1]); int errno_copy; static_assert(sizeof(errno_copy) == sizeof(errno), "errno size mismatch"); ssize_t ret = read(pipe_fds[0], &errno_copy, sizeof(errno_copy)); close(pipe_fds[0]); if (ret == 0) { reaper_add(reaper, pid, cb, cb_data); return pid; } else if (ret < 0) { LOG_ERRNO("failed to read from pipe"); return -1; } else { LOG_ERRNO_P(errno_copy, "%s: failed to spawn", argv[0]); errno = errno_copy; waitpid(pid, NULL, 0); return -1; } err: if (pipe_fds[0] != -1) close(pipe_fds[0]); if (pipe_fds[1] != -1) close(pipe_fds[1]); return -1; } bool spawn_expand_template(const struct config_spawn_template *template, size_t key_count, const char *key_names[static key_count], const char *key_values[static key_count], size_t *argc, char ***argv) { *argc = 0; *argv = NULL; for (; template->argv.args[*argc] != NULL; (*argc)++) ; #define append(s, n) \ do { \ expanded = xrealloc(expanded, len + (n) + 1); \ memcpy(&expanded[len], s, n); \ len += n; \ expanded[len] = '\0'; \ } while (0) *argv = xmalloc((*argc + 1) * sizeof((*argv)[0])); /* Expand the provided keys */ for (size_t i = 0; i < *argc; i++) { size_t len = 0; char *expanded = NULL; char *start = NULL; char *last_end = template->argv.args[i]; while ((start = strstr(last_end, "${")) != NULL) { /* Append everything from the last template's end to this * one's beginning */ append(last_end, start - last_end); /* Find end of template */ start += 2; char *end = strstr(start, "}"); if (end == NULL) { /* Ensure final append() copies the unclosed '${' */ last_end = start - 2; LOG_WARN("notify: unclosed template: %s", last_end); break; } /* Expand template */ bool valid_key = false; for (size_t j = 0; j < key_count; j++) { if (strncmp(start, key_names[j], end - start) != 0) continue; append(key_values[j], strlen(key_values[j])); valid_key = true; break; } if (!valid_key) { /* Unrecognized template - append it as-is */ start -= 2; append(start, end + 1 - start); LOG_WARN("notify: unrecognized template: %.*s", (int)(end + 1 - start), start); } last_end = end + 1; } append( last_end, template->argv.args[i] + strlen(template->argv.args[i]) - last_end); (*argv)[i] = expanded; } (*argv)[*argc] = NULL; #undef append return true; } foot-1.21.0/spawn.h000066400000000000000000000010061476600145200140450ustar00rootroot00000000000000#pragma once #include #include #include "config.h" #include "reaper.h" pid_t spawn(struct reaper *reaper, const char *cwd, char *const argv[], int stdin_fd, int stdout_fd, int stderr_fd, reaper_cb cb, void *cb_data, const char *xdg_activation_token); bool spawn_expand_template( const struct config_spawn_template *template, size_t key_count, const char *key_names[static key_count], const char *key_values[static key_count], size_t *argc, char ***argv); foot-1.21.0/stride.h000066400000000000000000000003061476600145200142110ustar00rootroot00000000000000#pragma once #include static inline int stride_for_format_and_width(pixman_format_code_t format, int width) { return (((PIXMAN_FORMAT_BPP(format) * width + 7) / 8 + 4 - 1) & -4); } foot-1.21.0/subprojects/000077500000000000000000000000001476600145200151125ustar00rootroot00000000000000foot-1.21.0/subprojects/fcft.wrap000066400000000000000000000001061476600145200167240ustar00rootroot00000000000000[wrap-git] url = https://codeberg.org/dnkl/fcft.git revision = master foot-1.21.0/subprojects/tllist.wrap000066400000000000000000000001101476600145200173100ustar00rootroot00000000000000[wrap-git] url = https://codeberg.org/dnkl/tllist.git revision = master foot-1.21.0/subprojects/wayland-protocols.wrap000066400000000000000000000001361476600145200214660ustar00rootroot00000000000000[wrap-git] url = https://gitlab.freedesktop.org/wayland/wayland-protocols.git revision = main foot-1.21.0/terminal.c000066400000000000000000004274211476600145200145400ustar00rootroot00000000000000#include "terminal.h" #if defined(__GLIBC__) #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "terminal" #define LOG_ENABLE_DBG 0 #include "log.h" #include "async.h" #include "commands.h" #include "config.h" #include "debug.h" #include "emoji-variation-sequences.h" #include "extract.h" #include "grid.h" #include "ime.h" #include "input.h" #include "notify.h" #include "quirks.h" #include "reaper.h" #include "render.h" #include "selection.h" #include "shm.h" #include "sixel.h" #include "slave.h" #include "spawn.h" #include "url-mode.h" #include "util.h" #include "vt.h" #include "xmalloc.h" #include "xsnprintf.h" #define PTMX_TIMING 0 static void enqueue_data_for_slave(const void *data, size_t len, size_t offset, ptmx_buffer_list_t *buffer_list) { struct ptmx_buffer queued = { .data = xmemdup(data, len), .len = len, .idx = offset, }; tll_push_back(*buffer_list, queued); } static bool data_to_slave(struct terminal *term, const void *data, size_t len, ptmx_buffer_list_t *buffer_list) { /* * Try a synchronous write first. If we fail to write everything, * switch to asynchronous. */ size_t async_idx = 0; switch (async_write(term->ptmx, data, len, &async_idx)) { case ASYNC_WRITE_REMAIN: /* Switch to asynchronous mode; let FDM write the remaining data */ if (!fdm_event_add(term->fdm, term->ptmx, EPOLLOUT)) return false; enqueue_data_for_slave(data, len, async_idx, buffer_list); return true; case ASYNC_WRITE_DONE: return true; case ASYNC_WRITE_ERR: LOG_ERRNO("failed to synchronously write %zu bytes to slave", len); return false; } BUG("Unexpected async_write() return value"); return false; } bool term_paste_data_to_slave(struct terminal *term, const void *data, size_t len) { xassert(term->is_sending_paste_data); if (term->ptmx < 0) { /* We're probably in "hold" */ return false; } if (tll_length(term->ptmx_paste_buffers) > 0) { /* Don't even try to send data *now* if there's queued up * data, since that would result in events arriving out of * order. */ enqueue_data_for_slave(data, len, 0, &term->ptmx_paste_buffers); return true; } return data_to_slave(term, data, len, &term->ptmx_paste_buffers); } bool term_to_slave(struct terminal *term, const void *data, size_t len) { if (term->ptmx < 0) { /* We're probably in "hold" */ return false; } if (tll_length(term->ptmx_buffers) > 0 || term->is_sending_paste_data) { /* * Don't even try to send data *now* if there's queued up * data, since that would result in events arriving out of * order. * * Furthermore, if we're currently sending paste data to the * client, do *not* mix that stream with other events * (https://codeberg.org/dnkl/foot/issues/101). */ enqueue_data_for_slave(data, len, 0, &term->ptmx_buffers); return true; } return data_to_slave(term, data, len, &term->ptmx_buffers); } static bool fdm_ptmx_out(struct fdm *fdm, int fd, int events, void *data) { struct terminal *term = data; /* If there is no queued data, then we shouldn't be in asynchronous mode */ xassert(tll_length(term->ptmx_buffers) > 0 || tll_length(term->ptmx_paste_buffers) > 0); /* Writes a single buffer, returns if not all of it could be written */ #define write_one_buffer(buffer_list) \ { \ switch (async_write(term->ptmx, it->item.data, it->item.len, &it->item.idx)) { \ case ASYNC_WRITE_DONE: \ free(it->item.data); \ tll_remove(buffer_list, it); \ break; \ case ASYNC_WRITE_REMAIN: \ /* to_slave() updated it->item.idx */ \ return true; \ case ASYNC_WRITE_ERR: \ LOG_ERRNO("failed to asynchronously write %zu bytes to slave", \ it->item.len - it->item.idx); \ return false; \ } \ } tll_foreach(term->ptmx_paste_buffers, it) write_one_buffer(term->ptmx_paste_buffers); /* If we get here, *all* paste data buffers were successfully * flushed */ if (!term->is_sending_paste_data) { tll_foreach(term->ptmx_buffers, it) write_one_buffer(term->ptmx_buffers); } /* * If we get here, *all* buffers were successfully flushed. * * Or, we're still sending paste data, in which case we do *not* * want to send the "normal" queued up data * * In both cases, we want to *disable* the FDM callback since * otherwise we'd just be called right away again, with nothing to * write. */ fdm_event_del(term->fdm, term->ptmx, EPOLLOUT); return true; } static bool add_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) { #if defined(UTMP_ADD) if (ptmx < 0) return true; if (conf->utmp_helper_path == NULL) return true; char *const argv[] = {conf->utmp_helper_path, UTMP_ADD, getenv("WAYLAND_DISPLAY"), NULL}; return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL, NULL, NULL) >= 0; #else return true; #endif } static bool del_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) { #if defined(UTMP_DEL) if (ptmx < 0) return true; if (conf->utmp_helper_path == NULL) return true; char *del_argument = #if defined(UTMP_DEL_HAVE_ARGUMENT) getenv("WAYLAND_DISPLAY") #else NULL #endif ; char *const argv[] = {conf->utmp_helper_path, UTMP_DEL, del_argument, NULL}; return spawn(reaper, NULL, argv, ptmx, ptmx, -1, NULL, NULL, NULL) >= 0; #else return true; #endif } #if PTMX_TIMING static struct timespec last = {0}; #endif static bool cursor_blink_rearm_timer(struct terminal *term); /* Externally visible, but not declared in terminal.h, to enable pgo * to call this function directly */ bool fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) { struct terminal *term = data; const bool pollin = events & EPOLLIN; const bool pollout = events & EPOLLOUT; const bool hup = events & EPOLLHUP; if (pollout) { if (!fdm_ptmx_out(fdm, fd, events, data)) return false; } /* Prevent blinking while typing */ if (term->cursor_blink.fd >= 0) { term->cursor_blink.state = CURSOR_BLINK_ON; cursor_blink_rearm_timer(term); } if (unlikely(term->interactive_resizing.grid != NULL)) { /* * Don't consume PTMX while we're doing an interactive resize, * since the 'normal' grid we're currently using is a * temporary one - all changes done to it will be lost when * the interactive resize ends. */ return true; } uint8_t buf[24 * 1024]; const size_t max_iterations = !hup ? 10 : SIZE_MAX; for (size_t i = 0; i < max_iterations && pollin; i++) { xassert(pollin); ssize_t count = read(term->ptmx, buf, sizeof(buf)); if (count < 0) { if (errno == EAGAIN || errno == EIO) { /* * EAGAIN: no more to read - FDM will trigger us again * EIO: assume PTY was closed - we already have, or will get, a EPOLLHUP */ break; } LOG_ERRNO("failed to read from pseudo terminal"); return false; } else if (count == 0) { /* Reached end-of-file */ break; } xassert(term->interactive_resizing.grid == NULL); vt_from_slave(term, buf, count); } if (!term->render.app_sync_updates.enabled) { /* * We likely need to re-render. But, we don't want to do it * immediately. Often, a single client update is done through * multiple writes. This could lead to us rendering one frame with * "intermediate" state. * * For example, we might end up rendering a frame * where the client just erased a line, while in the * next frame, the client wrote to the same line. This * causes screen "flickering". * * Mitigate by always incuring a small delay before * rendering the next frame. This gives the client * some time to finish the operation (and thus gives * us time to receive the last writes before doing any * actual rendering). * * We incur this delay *every* time we receive * input. To ensure we don't delay rendering * indefinitely, we start a second timer that is only * reset when we render. * * Note that when the client is producing data at a * very high pace, we're rate limited by the wayland * compositor anyway. The delay we introduce here only * has any effect when the renderer is idle. */ uint64_t lower_ns = term->conf->tweak.delayed_render_lower_ns; uint64_t upper_ns = term->conf->tweak.delayed_render_upper_ns; if (lower_ns > 0 && upper_ns > 0) { #if PTMX_TIMING struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); if (last.tv_sec > 0 || last.tv_nsec > 0) { struct timespec diff; timespec_sub(&now, &last, &diff); LOG_INFO("waited %lds %ldns for more input", (long)diff.tv_sec, diff.tv_nsec); } last = now; #endif xassert(lower_ns < 1000000000); xassert(upper_ns < 1000000000); xassert(upper_ns > lower_ns); timerfd_settime( term->delayed_render_timer.lower_fd, 0, &(struct itimerspec){.it_value = {.tv_nsec = lower_ns}}, NULL); /* Second timeout - only reset when we render. Set to one * frame (assuming 60Hz) */ if (!term->delayed_render_timer.is_armed) { timerfd_settime( term->delayed_render_timer.upper_fd, 0, &(struct itimerspec){.it_value = {.tv_nsec = upper_ns}}, NULL); term->delayed_render_timer.is_armed = true; } } else render_refresh(term); } if (hup) { del_utmp_record(term->conf, term->reaper, term->ptmx); fdm_del(fdm, fd); term->ptmx = -1; /* * Normally, we do *not* want to shutdown when the PTY is * closed. Instead, we want to wait for the client application * to exit. * * However, when we're using a pre-existing PTY (the --pty * option), there _is_ no client application. That is, foot * does *not* fork+exec anything, and thus the only way to * shutdown is to wait for the PTY to be closed. */ if (term->slave < 0 && !term->conf->hold_at_exit) { term_shutdown(term); } } return true; } bool term_ptmx_pause(struct terminal *term) { if (term->ptmx < 0) return false; return fdm_event_del(term->fdm, term->ptmx, EPOLLIN); } bool term_ptmx_resume(struct terminal *term) { if (term->ptmx < 0) return false; return fdm_event_add(term->fdm, term->ptmx, EPOLLIN); } static bool fdm_flash(struct fdm *fdm, int fd, int events, void *data) { if (events & EPOLLHUP) return false; struct terminal *term = data; uint64_t expiration_count; ssize_t ret = read( term->flash.fd, &expiration_count, sizeof(expiration_count)); if (ret < 0) { if (errno == EAGAIN) return true; LOG_ERRNO("failed to read flash timer"); return false; } LOG_DBG("flash timer expired %llu times", (unsigned long long)expiration_count); term->flash.active = false; render_overlay(term); // since the overlay surface is synced with the main window surface, we have // to commit the main surface for the compositor to acknowledge the new // overlay state. wl_surface_commit(term->window->surface.surf); return true; } static bool fdm_blink(struct fdm *fdm, int fd, int events, void *data) { if (events & EPOLLHUP) return false; struct terminal *term = data; uint64_t expiration_count; ssize_t ret = read( term->blink.fd, &expiration_count, sizeof(expiration_count)); if (ret < 0) { if (errno == EAGAIN) return true; LOG_ERRNO("failed to read blink timer"); return false; } LOG_DBG("blink timer expired %llu times", (unsigned long long)expiration_count); /* Invert blink state */ term->blink.state = term->blink.state == BLINK_ON ? BLINK_OFF : BLINK_ON; /* Scan all visible cells and mark rows with blinking cells dirty */ bool no_blinking_cells = true; for (int r = 0; r < term->rows; r++) { struct row *row = grid_row_in_view(term->grid, r); for (int col = 0; col < term->cols; col++) { struct cell *cell = &row->cells[col]; if (cell->attrs.blink) { cell->attrs.clean = 0; row->dirty = true; no_blinking_cells = false; } } } if (no_blinking_cells) { LOG_DBG("disarming blink timer"); term->blink.state = BLINK_ON; fdm_del(term->fdm, term->blink.fd); term->blink.fd = -1; } else render_refresh(term); return true; } void term_arm_blink_timer(struct terminal *term) { if (term->blink.fd >= 0) return; LOG_DBG("arming blink timer"); int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (fd < 0) { LOG_ERRNO("failed to create blink timer FD"); return; } if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_blink, term)) { close(fd); return; } struct itimerspec alarm = { .it_value = {.tv_sec = 0, .tv_nsec = 500 * 1000000}, .it_interval = {.tv_sec = 0, .tv_nsec = 500 * 1000000}, }; if (timerfd_settime(fd, 0, &alarm, NULL) < 0) { LOG_ERRNO("failed to arm blink timer"); fdm_del(term->fdm, fd); } term->blink.fd = fd; } static void cursor_refresh(struct terminal *term) { if (!term->window->is_configured) return; term->grid->cur_row->cells[term->grid->cursor.point.col].attrs.clean = 0; term->grid->cur_row->dirty = true; render_refresh(term); } static bool fdm_cursor_blink(struct fdm *fdm, int fd, int events, void *data) { if (events & EPOLLHUP) return false; struct terminal *term = data; uint64_t expiration_count; ssize_t ret = read( term->cursor_blink.fd, &expiration_count, sizeof(expiration_count)); if (ret < 0) { if (errno == EAGAIN) return true; LOG_ERRNO("failed to read cursor blink timer"); return false; } LOG_DBG("cursor blink timer expired %llu times", (unsigned long long)expiration_count); /* Invert blink state */ term->cursor_blink.state = term->cursor_blink.state == CURSOR_BLINK_ON ? CURSOR_BLINK_OFF : CURSOR_BLINK_ON; cursor_refresh(term); return true; } static bool fdm_delayed_render(struct fdm *fdm, int fd, int events, void *data) { if (events & EPOLLHUP) return false; struct terminal *term = data; uint64_t unused; ssize_t ret1 = 0; ssize_t ret2 = 0; if (fd == term->delayed_render_timer.lower_fd) ret1 = read(term->delayed_render_timer.lower_fd, &unused, sizeof(unused)); if (fd == term->delayed_render_timer.upper_fd) ret2 = read(term->delayed_render_timer.upper_fd, &unused, sizeof(unused)); if ((ret1 < 0 || ret2 < 0)) { if (errno == EAGAIN) return true; LOG_ERRNO("failed to read timeout timer"); return false; } if (ret1 > 0) LOG_DBG("lower delay timer expired"); else if (ret2 > 0) LOG_DBG("upper delay timer expired"); if (ret1 == 0 && ret2 == 0) return true; #if PTMX_TIMING last = (struct timespec){0}; #endif /* Reset timers */ struct itimerspec reset = {{0}}; timerfd_settime(term->delayed_render_timer.lower_fd, 0, &reset, NULL); timerfd_settime(term->delayed_render_timer.upper_fd, 0, &reset, NULL); term->delayed_render_timer.is_armed = false; render_refresh(term); return true; } static bool fdm_app_sync_updates_timeout( struct fdm *fdm, int fd, int events, void *data) { if (events & EPOLLHUP) return false; struct terminal *term = data; uint64_t unused; ssize_t ret = read(term->render.app_sync_updates.timer_fd, &unused, sizeof(unused)); if (ret < 0) { if (errno == EAGAIN) return true; LOG_ERRNO("failed to read application synchronized updates timeout timer"); return false; } term_disable_app_sync_updates(term); return true; } static bool fdm_title_update_timeout(struct fdm *fdm, int fd, int events, void *data) { if (events & EPOLLHUP) return false; struct terminal *term = data; uint64_t unused; ssize_t ret = read(term->render.title.timer_fd, &unused, sizeof(unused)); if (ret < 0) { if (errno == EAGAIN) return true; LOG_ERRNO("failed to read title update throttle timer"); return false; } struct itimerspec reset = {{0}}; timerfd_settime(term->render.title.timer_fd, 0, &reset, NULL); render_refresh_title(term); return true; } static bool fdm_icon_update_timeout(struct fdm *fdm, int fd, int events, void *data) { if (events & EPOLLHUP) return false; struct terminal *term = data; uint64_t unused; ssize_t ret = read(term->render.icon.timer_fd, &unused, sizeof(unused)); if (ret < 0) { if (errno == EAGAIN) return true; LOG_ERRNO("failed to read icon update throttle timer"); return false; } struct itimerspec reset = {{0}}; timerfd_settime(term->render.icon.timer_fd, 0, &reset, NULL); render_refresh_icon(term); return true; } static bool fdm_app_id_update_timeout(struct fdm *fdm, int fd, int events, void *data) { if (events & EPOLLHUP) return false; struct terminal *term = data; uint64_t unused; ssize_t ret = read(term->render.app_id.timer_fd, &unused, sizeof(unused)); if (ret < 0) { if (errno == EAGAIN) return true; LOG_ERRNO("failed to read app ID update throttle timer"); return false; } struct itimerspec reset = {{0}}; timerfd_settime(term->render.app_id.timer_fd, 0, &reset, NULL); render_refresh_app_id(term); return true; } static bool initialize_render_workers(struct terminal *term) { LOG_INFO("using %hu rendering threads", term->render.workers.count); if (sem_init(&term->render.workers.start, 0, 0) < 0 || sem_init(&term->render.workers.done, 0, 0) < 0) { LOG_ERRNO("failed to instantiate render worker semaphores"); return false; } int err; if ((err = mtx_init(&term->render.workers.lock, mtx_plain)) != thrd_success) { LOG_ERR("failed to instantiate render worker mutex: %s (%d)", thrd_err_as_string(err), err); goto err_sem_destroy; } term->render.workers.threads = xcalloc( term->render.workers.count, sizeof(term->render.workers.threads[0])); for (size_t i = 0; i < term->render.workers.count; i++) { struct render_worker_context *ctx = xmalloc(sizeof(*ctx)); *ctx = (struct render_worker_context) { .term = term, .my_id = 1 + i, }; int ret = thrd_create( &term->render.workers.threads[i], &render_worker_thread, ctx); if (ret != thrd_success) { LOG_ERR("failed to create render worker thread: %s (%d)", thrd_err_as_string(ret), ret); term->render.workers.threads[i] = 0; return false; } } return true; err_sem_destroy: sem_destroy(&term->render.workers.start); sem_destroy(&term->render.workers.done); return false; } static void free_custom_glyph(struct fcft_glyph **glyph) { if (*glyph == NULL) return; free(pixman_image_get_data((*glyph)->pix)); pixman_image_unref((*glyph)->pix); free(*glyph); *glyph = NULL; } static void free_custom_glyphs(struct fcft_glyph ***glyphs, size_t count) { if (*glyphs == NULL) return; for (size_t i = 0; i < count; i++) free_custom_glyph(&(*glyphs)[i]); free(*glyphs); *glyphs = NULL; } static void term_line_height_update(struct terminal *term) { const struct config *conf = term->conf; if (term->conf->line_height.px < 0) { term->font_line_height.pt = 0; term->font_line_height.px = -1; return; } const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; const float font_original_pt_size = conf->fonts[0].arr[0].px_size > 0 ? conf->fonts[0].arr[0].px_size * 72. / dpi : conf->fonts[0].arr[0].pt_size; const float font_current_pt_size = term->font_sizes[0][0].px_size > 0 ? term->font_sizes[0][0].px_size * 72. / dpi : term->font_sizes[0][0].pt_size; const float change = font_current_pt_size / font_original_pt_size; const float line_original_pt_size = conf->line_height.px > 0 ? conf->line_height.px * 72. / dpi : conf->line_height.pt; term->font_line_height.px = 0; term->font_line_height.pt = fmaxf(line_original_pt_size * change, 0.); } static bool term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4], bool resize_grid) { for (size_t i = 0; i < 4; i++) { xassert(fonts[i] != NULL); fcft_destroy(term->fonts[i]); term->fonts[i] = fonts[i]; } free_custom_glyphs( &term->custom_glyphs.box_drawing, GLYPH_BOX_DRAWING_COUNT); free_custom_glyphs( &term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT); free_custom_glyphs( &term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); free_custom_glyphs( &term->custom_glyphs.octants, GLYPH_OCTANTS_COUNT); const struct config *conf = term->conf; const struct fcft_glyph *M = fcft_rasterize_char_utf32( fonts[0], U'M', term->font_subpixel); int advance = M != NULL ? M->advance.x : term->fonts[0]->max_advance.x; term_line_height_update(term); term->cell_width = advance + term_pt_or_px_as_pixels(term, &conf->letter_spacing); term->cell_height = term->font_line_height.px >= 0 ? term_pt_or_px_as_pixels(term, &term->font_line_height) : max(term->fonts[0]->height, term->fonts[0]->ascent + term->fonts[0]->descent); if (term->cell_width <= 0) term->cell_width = 1; if (term->cell_height <= 0) term->cell_height = 1; term->font_x_ofs = term_pt_or_px_as_pixels(term, &conf->horizontal_letter_offset); term->font_y_ofs = term_pt_or_px_as_pixels(term, &conf->vertical_letter_offset); term->font_baseline = term_font_baseline(term); LOG_INFO("cell width=%d, height=%d", term->cell_width, term->cell_height); sixel_cell_size_changed(term); /* Optimization - some code paths (are forced to) call * render_resize() after this function */ if (resize_grid) { /* Use force, since cell-width/height may have changed */ enum resize_options resize_opts = RESIZE_FORCE; if (conf->resize_keep_grid) resize_opts |= RESIZE_KEEP_GRID; render_resize( term, (int)roundf(term->width / term->scale), (int)roundf(term->height / term->scale), resize_opts); } return true; } static float get_font_dpi(const struct terminal *term) { /* * Use output's DPI to scale font. This is to ensure the font has * the same physical height (if measured by a ruler) regardless of * monitor. * * Conceptually, we use the physical monitor specs to calculate * the DPI, and we ignore the output's scaling factor. * * However, to deal with legacy fractional scaling, where we're * told to render at e.g. 2x, but are then downscaled by the * compositor to e.g. 1.25, we use the scaled DPI value multiplied * by the scale factor instead. * * For integral scaling factors the resulting DPI is the same as * if we had used the physical DPI. * * For legacy fractional scaling factors we'll get a DPI *larger* * than the physical DPI, that ends up being right when later * downscaled by the compositor. * * With the newer fractional-scale-v1 protocol, we use the * monitor's real DPI, since we scale everything to the correct * scaling factor (no downscaling done by the compositor). */ const struct wl_window *win = term->window; const struct monitor *mon = NULL; if (tll_length(win->on_outputs) > 0) mon = tll_back(win->on_outputs); else { if (term->font_dpi_before_unmap > 0.) { /* * Use last known "good" DPI * * This avoids flickering when window is unmapped/mapped * (some compositors do this when a window is minimized), * on a multi-monitor setup with different monitor DPIs. */ return term->font_dpi_before_unmap; } if (tll_length(term->wl->monitors) > 0) mon = &tll_front(term->wl->monitors); } const float monitor_dpi = mon != NULL ? term_fractional_scaling(term) ? mon->dpi.physical : mon->dpi.scaled : 96.; return monitor_dpi > 0. ? monitor_dpi : 96.; } static enum fcft_subpixel get_font_subpixel(const struct terminal *term) { if (term->colors.alpha != 0xffff) { /* Can't do subpixel rendering on transparent background */ return FCFT_SUBPIXEL_NONE; } enum wl_output_subpixel wl_subpixel; /* * Wayland doesn't tell us *which* part of the surface that goes * on a specific output, only whether the surface is mapped to an * output or not. * * Thus, when determining which subpixel mode to use, we can't do * much but select *an* output. So, we pick the one we were most * recently mapped on. * * If we're not mapped at all, we pick the first available * monitor, and hope that's where we'll eventually get mapped. * * If there aren't any monitors we use the "default" subpixel * mode. */ if (tll_length(term->window->on_outputs) > 0) wl_subpixel = tll_back(term->window->on_outputs)->subpixel; else if (tll_length(term->wl->monitors) > 0) wl_subpixel = tll_front(term->wl->monitors).subpixel; else wl_subpixel = WL_OUTPUT_SUBPIXEL_UNKNOWN; switch (wl_subpixel) { case WL_OUTPUT_SUBPIXEL_UNKNOWN: return FCFT_SUBPIXEL_DEFAULT; case WL_OUTPUT_SUBPIXEL_NONE: return FCFT_SUBPIXEL_NONE; case WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB: return FCFT_SUBPIXEL_HORIZONTAL_RGB; case WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR: return FCFT_SUBPIXEL_HORIZONTAL_BGR; case WL_OUTPUT_SUBPIXEL_VERTICAL_RGB: return FCFT_SUBPIXEL_VERTICAL_RGB; case WL_OUTPUT_SUBPIXEL_VERTICAL_BGR: return FCFT_SUBPIXEL_VERTICAL_BGR; } return FCFT_SUBPIXEL_DEFAULT; } int term_pt_or_px_as_pixels(const struct terminal *term, const struct pt_or_px *pt_or_px) { float scale = !term->font_is_sized_by_dpi ? term->scale : 1.; float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; return pt_or_px->px == 0 ? (int)roundf(pt_or_px->pt * scale * dpi / 72) : (int)roundf(pt_or_px->px * scale); } struct font_load_data { size_t count; const char **names; const char *attrs; const struct fcft_font_options *options; struct fcft_font **font; }; static int font_loader_thread(void *_data) { struct font_load_data *data = _data; *data->font = fcft_from_name2( data->count, data->names, data->attrs, data->options); return *data->font != NULL; } static bool reload_fonts(struct terminal *term, bool resize_grid) { const struct config *conf = term->conf; const size_t counts[4] = { conf->fonts[0].count, conf->fonts[1].count, conf->fonts[2].count, conf->fonts[3].count, }; /* Configure size (which may have been changed run-time) */ char **names[4]; for (size_t i = 0; i < 4; i++) { names[i] = xmalloc(counts[i] * sizeof(names[i][0])); const struct config_font_list *font_list = &conf->fonts[i]; for (size_t j = 0; j < font_list->count; j++) { const struct config_font *font = &font_list->arr[j]; bool use_px_size = term->font_sizes[i][j].px_size > 0; char size[64]; const float scale = term->font_is_sized_by_dpi ? 1. : term->scale; if (use_px_size) snprintf(size, sizeof(size), ":pixelsize=%d", (int)roundf(term->font_sizes[i][j].px_size * scale)); else snprintf(size, sizeof(size), ":size=%.2f", term->font_sizes[i][j].pt_size * scale); names[i][j] = xstrjoin(font->pattern, size); } } /* Did user configure custom bold/italic fonts? * Or should we use the regular font, with weight/slant attributes? */ const bool custom_bold = counts[1] > 0; const bool custom_italic = counts[2] > 0; const bool custom_bold_italic = counts[3] > 0; const size_t count_regular = counts[0]; const char **names_regular = (const char **)names[0]; const size_t count_bold = custom_bold ? counts[1] : counts[0]; const char **names_bold = (const char **)(custom_bold ? names[1] : names[0]); const size_t count_italic = custom_italic ? counts[2] : counts[0]; const char **names_italic = (const char **)(custom_italic ? names[2] : names[0]); const size_t count_bold_italic = custom_bold_italic ? counts[3] : counts[0]; const char **names_bold_italic = (const char **)(custom_bold_italic ? names[3] : names[0]); const bool use_dpi = term->font_is_sized_by_dpi; char *dpi = xasprintf("dpi=%.2f", use_dpi ? term->font_dpi : 96.); char *attrs[4] = { [0] = dpi, /* Takes ownership */ [1] = xstrjoin(dpi, !custom_bold ? ":weight=bold" : ""), [2] = xstrjoin(dpi, !custom_italic ? ":slant=italic" : ""), [3] = xstrjoin(dpi, !custom_bold_italic ? ":weight=bold:slant=italic" : ""), }; struct fcft_font_options *options = fcft_font_options_create(); options->scaling_filter = conf->tweak.fcft_filter; options->color_glyphs.format = PIXMAN_a8r8g8b8; options->color_glyphs.srgb_decode = render_do_linear_blending(term); if (conf->tweak.surface_bit_depth == SHM_10_BIT) { if ((term->wl->shm_have_argb2101010 && term->wl->shm_have_xrgb2101010) || (term->wl->shm_have_abgr2101010 && term->wl->shm_have_xbgr2101010)) { /* * 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. */ options->color_glyphs.format = PIXMAN_rgba_float; } } struct fcft_font *fonts[4]; struct font_load_data data[4] = { {count_regular, names_regular, attrs[0], options, &fonts[0]}, {count_bold, names_bold, attrs[1], options, &fonts[1]}, {count_italic, names_italic, attrs[2], options, &fonts[2]}, {count_bold_italic, names_bold_italic, attrs[3], options, &fonts[3]}, }; thrd_t tids[4] = {0}; for (size_t i = 0; i < 4; i++) { int ret = thrd_create(&tids[i], &font_loader_thread, &data[i]); if (ret != thrd_success) { LOG_ERR("failed to create font loader thread: %s (%d)", thrd_err_as_string(ret), ret); break; } } bool success = true; for (size_t i = 0; i < 4; i++) { if (tids[i] != 0) { int ret; if (thrd_join(tids[i], &ret) != thrd_success) success = false; else success = success && ret; } else success = false; } fcft_font_options_destroy(options); for (size_t i = 0; i < 4; i++) { for (size_t j = 0; j < counts[i]; j++) free(names[i][j]); free(names[i]); free(attrs[i]); } if (!success) { LOG_ERR("failed to load primary fonts"); for (size_t i = 0; i < 4; i++) { fcft_destroy(fonts[i]); fonts[i] = NULL; } } return success ? term_set_fonts(term, fonts, resize_grid) : success; } static bool load_fonts_from_conf(struct terminal *term) { const struct config *conf = term->conf; for (size_t i = 0; i < 4; i++) { const struct config_font_list *font_list = &conf->fonts[i]; for (size_t j = 0; j < font_list->count; j++) { const struct config_font *font = &font_list->arr[j]; term->font_sizes[i][j] = (struct config_font){ .pt_size = font->pt_size, .px_size = font->px_size}; } } return reload_fonts(term, true); } static void fdm_client_terminated( struct reaper *reaper, pid_t pid, int status, void *data); static const int PTY_OPEN_FLAGS = O_RDWR | O_NOCTTY; struct terminal * term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl, const char *foot_exe, const char *cwd, const char *token, const char *pty_path, int argc, char *const *argv, const char *const *envp, void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data) { int ptmx = -1; int flash_fd = -1; int delay_lower_fd = -1; int delay_upper_fd = -1; int app_sync_updates_fd = -1; int title_update_fd = -1; int icon_update_fd = -1; int app_id_update_fd = -1; struct terminal *term = malloc(sizeof(*term)); if (unlikely(term == NULL)) { LOG_ERRNO("malloc() failed"); return NULL; } ptmx = pty_path ? open(pty_path, PTY_OPEN_FLAGS) : posix_openpt(PTY_OPEN_FLAGS); if (ptmx < 0) { LOG_ERRNO("failed to open PTY"); goto close_fds; } if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { LOG_ERRNO("failed to create flash timer FD"); goto close_fds; } if ((delay_lower_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || (delay_upper_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { LOG_ERRNO("failed to create delayed rendering timer FDs"); goto close_fds; } if ((app_sync_updates_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { LOG_ERRNO("failed to create application synchronized updates timer FD"); goto close_fds; } if ((title_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { LOG_ERRNO("failed to create title update throttle timer FD"); goto close_fds; } if ((icon_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { LOG_ERRNO("failed to create icon update throttle timer FD"); goto close_fds; } if ((app_id_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { LOG_ERRNO("failed to create app ID update throttle timer FD"); goto close_fds; } if (ioctl(ptmx, (unsigned int)TIOCSWINSZ, &(struct winsize){.ws_row = 24, .ws_col = 80}) < 0) { LOG_ERRNO("failed to set initial TIOCSWINSZ"); goto close_fds; } /* Need to register *very* early (before the first "goto err"), to * ensure term_destroy() doesn't unref a key-binding we haven't * yet ref:d */ key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf); int ptmx_flags; if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 || fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0) { LOG_ERRNO("failed to configure ptmx as non-blocking"); goto err; } /* * Enable all FDM callbackes *except* ptmx - we can't do that * until the window has been 'configured' since we don't have a * size (and thus no grid) before then. */ if (!fdm_add(fdm, flash_fd, EPOLLIN, &fdm_flash, term) || !fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) || !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) || !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout, term) || !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term) || !fdm_add(fdm, icon_update_fd, EPOLLIN, &fdm_icon_update_timeout, term) || !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, term)) { goto err; } const bool ten_bit_surfaces = conf->tweak.surface_bit_depth == SHM_10_BIT; /* Initialize configure-based terminal attributes */ *term = (struct terminal) { .fdm = fdm, .reaper = reaper, .conf = conf, .slave = -1, .ptmx = ptmx, .ptmx_buffers = tll_init(), .ptmx_paste_buffers = tll_init(), .font_sizes = { xmalloc(sizeof(term->font_sizes[0][0]) * conf->fonts[0].count), xmalloc(sizeof(term->font_sizes[1][0]) * conf->fonts[1].count), xmalloc(sizeof(term->font_sizes[2][0]) * conf->fonts[2].count), xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count), }, .font_dpi = 0., .font_dpi_before_unmap = -1., .font_subpixel = (conf->colors.alpha == 0xffff /* Can't do subpixel rendering on transparent background */ ? FCFT_SUBPIXEL_DEFAULT : FCFT_SUBPIXEL_NONE), .cursor_keys_mode = CURSOR_KEYS_NORMAL, .keypad_keys_mode = KEYPAD_NUMERICAL, .reverse_wrap = true, .auto_margin = true, .window_title_stack = tll_init(), .scale = 1., .scale_before_unmap = -1, .flash = {.fd = flash_fd}, .blink = {.fd = -1}, .vt = { .state = 0, /* STATE_GROUND */ }, .colors = { .fg = conf->colors.fg, .bg = conf->colors.bg, .alpha = conf->colors.alpha, .cursor_fg = conf->cursor.color.text, .cursor_bg = conf->cursor.color.cursor, .selection_fg = conf->colors.selection_fg, .selection_bg = conf->colors.selection_bg, .use_custom_selection = conf->colors.use_custom.selection, }, .color_stack = { .stack = NULL, .size = 0, .idx = 0, }, .origin = ORIGIN_ABSOLUTE, .cursor_style = conf->cursor.style, .cursor_blink = { .decset = false, .deccsusr = conf->cursor.blink.enabled, .state = CURSOR_BLINK_ON, .fd = -1, }, .selection = { .coords = { .start = {-1, -1}, .end = {-1, -1}, }, .pivot = { .start = {-1, -1}, .end = {-1, -1}, }, .auto_scroll = { .fd = -1, }, }, .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, .grid = &term->normal, .composed = NULL, .alt_scrolling = conf->mouse.alternate_scroll_mode, .meta = { .esc_prefix = true, .eight_bit = true, }, .num_lock_modifier = true, .bell_action_enabled = true, .tab_stops = tll_init(), .wl = wayl, .render = { .chains = { .grid = shm_chain_new(wayl, true, 1 + conf->render_worker_count, ten_bit_surfaces), .search = shm_chain_new(wayl, false, 1 ,ten_bit_surfaces), .scrollback_indicator = shm_chain_new(wayl, false, 1, ten_bit_surfaces), .render_timer = shm_chain_new(wayl, false, 1, ten_bit_surfaces), .url = shm_chain_new(wayl, false, 1, ten_bit_surfaces), .csd = shm_chain_new(wayl, false, 1, ten_bit_surfaces), .overlay = shm_chain_new(wayl, false, 1, ten_bit_surfaces), }, .scrollback_lines = conf->scrollback.lines, .app_sync_updates.timer_fd = app_sync_updates_fd, .title = { .timer_fd = title_update_fd, }, .icon = { .timer_fd = icon_update_fd, }, .app_id = { .timer_fd = app_id_update_fd, }, .workers = { .count = conf->render_worker_count, .queue = tll_init(), }, }, .delayed_render_timer = { .is_armed = false, .lower_fd = delay_lower_fd, .upper_fd = delay_upper_fd, }, .sixel = { .scrolling = true, .use_private_palette = true, .palette_size = SIXEL_MAX_COLORS, .max_width = SIXEL_MAX_WIDTH, .max_height = SIXEL_MAX_HEIGHT, }, .shutdown = { .terminate_timeout_fd = -1, .cb = shutdown_cb, .cb_data = shutdown_data, }, .foot_exe = xstrdup(foot_exe), .cwd = xstrdup(cwd), .grapheme_shaping = conf->tweak.grapheme_shaping, #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED .ime_enabled = true, #endif .active_notifications = tll_init(), }; pixman_region32_init(&term->render.last_overlay_clip); term_update_ascii_printer(term); for (size_t i = 0; i < 4; i++) { const struct config_font_list *font_list = &conf->fonts[i]; for (size_t j = 0; j < font_list->count; j++) { const struct config_font *font = &font_list->arr[j]; term->font_sizes[i][j] = (struct config_font){ .pt_size = font->pt_size, .px_size = font->px_size}; } } for (size_t i = 0; i < ALEN(term->notification_icons); i++) { term->notification_icons[i].tmp_file_fd = -1; } add_utmp_record(conf, reaper, ptmx); if (!pty_path) { /* Start the slave/client */ if ((term->slave = slave_spawn( term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, conf->term, conf->shell, conf->login_shell, &conf->notifications)) == -1) { goto err; } reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); } /* Guess scale; we're not mapped yet, so we don't know on which * output we'll be. Use scaling factor from first monitor */ xassert(tll_length(term->wl->monitors) > 0); term->scale = tll_front(term->wl->monitors).scale; memcpy(term->colors.table, term->conf->colors.table, sizeof(term->colors.table)); /* Initialize the Wayland window backend */ if ((term->window = wayl_win_init(term, token)) == NULL) goto err; /* Load fonts */ if (!term_font_dpi_changed(term, 0.)) goto err; term->font_subpixel = get_font_subpixel(term); term_set_window_title(term, conf->title); /* Let the Wayland backend know we exist */ tll_push_back(wayl->terms, term); switch (conf->startup_mode) { case STARTUP_WINDOWED: break; case STARTUP_MAXIMIZED: xdg_toplevel_set_maximized(term->window->xdg_toplevel); break; case STARTUP_FULLSCREEN: xdg_toplevel_set_fullscreen(term->window->xdg_toplevel, NULL); break; } if (!initialize_render_workers(term)) goto err; return term; err: term->shutdown.in_progress = true; term_destroy(term); return NULL; close_fds: close(ptmx); fdm_del(fdm, flash_fd); fdm_del(fdm, delay_lower_fd); fdm_del(fdm, delay_upper_fd); fdm_del(fdm, app_sync_updates_fd); fdm_del(fdm, title_update_fd); fdm_del(fdm, icon_update_fd); fdm_del(fdm, app_id_update_fd); free(term); return NULL; } void term_window_configured(struct terminal *term) { /* Enable ptmx FDM callback */ if (!term->shutdown.in_progress) { xassert(term->window->is_configured); fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); const bool gamma_correct = render_do_linear_blending(term); LOG_INFO("gamma-correct blending: %s", gamma_correct ? "enabled" : "disabled"); } } /* * Shutdown logic * * A foot instance can be terminated in two ways: * * - the client application terminates (user types 'exit', or pressed C-d in the * shell, etc) * - the foot window is closed * * Both variants need to trigger to "other" action. I.e. if the client * application is terminated, then we need to close the window. If the window is * closed, we need to terminate the client application. * * Only when *both* tasks have completed do we consider ourselves fully * shutdown. This is when we can call term_destroy(), and the user provided * shutdown callback. * * The functions involved with this are: * * - shutdown_maybe_done(): called after any of the two tasks above have * completed. When it determines that *both* tasks are done, it calls * term_destroy() and the user provided shutdown callback. * * - fdm_client_terminated(): reaper callback, called when the client * application has terminated. * * + Kills the "terminate" timeout timer * + Calls shutdown_maybe_done() if the shutdown procedure has already * started (i.e. the window being closed initiated the shutdown) * -OR- * Initiates the shutdown itself, by calling term_shutdown() (client * application termination initiated the shutdown). * * - term_shutdown(): unregisters all FDM callbacks, sends SIGTERM to the client * application and installs a "terminate" timeout timer (if it hasn't already * terminated). Finally registers an event FD with the FDM, which is * immediately triggered. This is done to ensure any pending FDM events are * handled before shutting down. * * - fdm_shutdown(): FDM callback, triggered by the event FD in * term_shutdown(). Unmaps and destroys the window resources, and ensures the * seats' focused pointers don't reference us. Finally calls * shutdown_maybe_done(). * * - fdm_terminate_timeout(): FDM callback for the "terminate" timeout * timer. This function is called when the client application hasn't * terminated after 60 seconds (after the SIGTERM). Sends SIGKILL to the * client application. * * - term_destroy(): normally called from shutdown_maybe_done(), when both the * window has been unmapped, and the client application has terminated. In * this case, it simply destroys all resources. * * It may however also be called without term_shutdown() having been called * (typically in error code paths - for example, when the Wayland connection * is closed by the compositor). In this case, the client application is * typically still running, and we can't assume the FDM is running. To handle * this, we install configure a 60 second SIGALRM, send SIGTERM to the client * application, and then enter a blocking waitpid(). * * If the alarm triggers, we send SIGKILL and once again enter a blocking * waitpid(). */ static void shutdown_maybe_done(struct terminal *term) { bool shutdown_done = term->window == NULL && term->shutdown.client_has_terminated; LOG_DBG("window=%p, slave-has-been-reaped=%d --> %s", (void *)term->window, term->shutdown.client_has_terminated, (shutdown_done ? "shutdown done, calling term_destroy()" : "no action")); if (!shutdown_done) return; void (*cb)(void *, int) = term->shutdown.cb; void *cb_data = term->shutdown.cb_data; int exit_code = term_destroy(term); if (cb != NULL) cb(cb_data, exit_code); } static void fdm_client_terminated(struct reaper *reaper, pid_t pid, int status, void *data) { struct terminal *term = data; LOG_DBG("slave (PID=%u) died", pid); term->shutdown.client_has_terminated = true; term->shutdown.exit_status = status; if (term->shutdown.terminate_timeout_fd >= 0) { fdm_del(term->fdm, term->shutdown.terminate_timeout_fd); term->shutdown.terminate_timeout_fd = -1; } if (term->shutdown.in_progress) shutdown_maybe_done(term); else if (!term->conf->hold_at_exit) term_shutdown(term); } static bool fdm_shutdown(struct fdm *fdm, int fd, int events, void *data) { struct terminal *term = data; /* Kill the event FD */ fdm_del(term->fdm, fd); wayl_win_destroy(term->window); term->window = NULL; struct wayland *wayl = term->wl; /* * Normally we'd get unmapped when we destroy the Wayland * above. * * However, it appears that under certain conditions, those events * are deferred (for example, when a screen locker is active), and * thus we can get here without having been unmapped. */ tll_foreach(wayl->seats, it) { if (it->item.kbd_focus == term) it->item.kbd_focus = NULL; if (it->item.mouse_focus == term) it->item.mouse_focus = NULL; } shutdown_maybe_done(term); return true; } static bool fdm_terminate_timeout(struct fdm *fdm, int fd, int events, void *data) { uint64_t unused; ssize_t bytes = read(fd, &unused, sizeof(unused)); if (bytes < 0) { LOG_ERRNO("failed to read from slave terminate timeout FD"); return false; } struct terminal *term = data; xassert(!term->shutdown.client_has_terminated); LOG_DBG("slave (PID=%u) has not terminated, sending %s (%d)", term->slave, term->shutdown.next_signal == SIGTERM ? "SIGTERM" : term->shutdown.next_signal == SIGKILL ? "SIGKILL" : "", term->shutdown.next_signal); kill(-term->slave, term->shutdown.next_signal); switch (term->shutdown.next_signal) { case SIGTERM: term->shutdown.next_signal = SIGKILL; break; case SIGKILL: /* Disarm. Shouldn't be necessary, as we should be able to shutdown completely after sending SIGKILL, before the next timeout occurs). But lets play it safe... */ if (term->shutdown.terminate_timeout_fd >= 0) { timerfd_settime( term->shutdown.terminate_timeout_fd, 0, &(const struct itimerspec){0}, NULL); } break; default: BUG("can only handle SIGTERM and SIGKILL"); return false; } return true; } bool term_shutdown(struct terminal *term) { if (term->shutdown.in_progress) return true; term->shutdown.in_progress = true; /* * Close FDs then postpone self-destruction to the next poll * iteration, by creating an event FD that we trigger immediately. */ term_cursor_blink_update(term); xassert(term->cursor_blink.fd < 0); fdm_del(term->fdm, term->selection.auto_scroll.fd); fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); fdm_del(term->fdm, term->render.app_id.timer_fd); fdm_del(term->fdm, term->render.icon.timer_fd); fdm_del(term->fdm, term->render.title.timer_fd); fdm_del(term->fdm, term->delayed_render_timer.lower_fd); fdm_del(term->fdm, term->delayed_render_timer.upper_fd); fdm_del(term->fdm, term->blink.fd); fdm_del(term->fdm, term->flash.fd); del_utmp_record(term->conf, term->reaper, term->ptmx); if (term->window != NULL && term->window->is_configured) fdm_del(term->fdm, term->ptmx); else close(term->ptmx); if (!term->shutdown.client_has_terminated) { if (term->slave <= 0) { term->shutdown.client_has_terminated = true; } else { LOG_DBG("initiating asynchronous terminate of slave; " "sending SIGHUP to PID=%u", term->slave); kill(-term->slave, SIGHUP); /* * Set up a timer, with an interval - on the first timeout * we'll send SIGTERM. If the the client application still * isn't terminating, we'll wait an additional interval, * and then send SIGKILL. */ const struct itimerspec timeout = {.it_value = {.tv_sec = 30}, .it_interval = {.tv_sec = 30}}; int timeout_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (timeout_fd < 0 || timerfd_settime(timeout_fd, 0, &timeout, NULL) < 0 || !fdm_add(term->fdm, timeout_fd, EPOLLIN, &fdm_terminate_timeout, term)) { if (timeout_fd >= 0) close(timeout_fd); LOG_ERRNO("failed to create slave terminate timeout FD"); return false; } xassert(term->shutdown.terminate_timeout_fd < 0); term->shutdown.terminate_timeout_fd = timeout_fd; term->shutdown.next_signal = SIGTERM; } } term->selection.auto_scroll.fd = -1; term->render.app_sync_updates.timer_fd = -1; term->render.app_id.timer_fd = -1; term->render.icon.timer_fd = -1; term->render.title.timer_fd = -1; term->delayed_render_timer.lower_fd = -1; term->delayed_render_timer.upper_fd = -1; term->blink.fd = -1; term->flash.fd = -1; term->ptmx = -1; int event_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); if (event_fd == -1) { LOG_ERRNO("failed to create terminal shutdown event FD"); return false; } if (!fdm_add(term->fdm, event_fd, EPOLLIN, &fdm_shutdown, term)) { close(event_fd); return false; } if (write(event_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t)) { LOG_ERRNO("failed to send terminal shutdown event"); fdm_del(term->fdm, event_fd); return false; } return true; } static volatile sig_atomic_t alarm_raised; static void sig_alarm(int signo) { LOG_DBG("SIGALRM"); alarm_raised = 1; } int term_destroy(struct terminal *term) { if (term == NULL) return 0; tll_foreach(term->wl->terms, it) { if (it->item == term) { tll_remove(term->wl->terms, it); break; } } del_utmp_record(term->conf, term->reaper, term->ptmx); fdm_del(term->fdm, term->selection.auto_scroll.fd); fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); fdm_del(term->fdm, term->render.app_id.timer_fd); fdm_del(term->fdm, term->render.icon.timer_fd); fdm_del(term->fdm, term->render.title.timer_fd); fdm_del(term->fdm, term->delayed_render_timer.lower_fd); fdm_del(term->fdm, term->delayed_render_timer.upper_fd); fdm_del(term->fdm, term->cursor_blink.fd); fdm_del(term->fdm, term->blink.fd); fdm_del(term->fdm, term->flash.fd); fdm_del(term->fdm, term->ptmx); if (term->shutdown.terminate_timeout_fd >= 0) fdm_del(term->fdm, term->shutdown.terminate_timeout_fd); if (term->window != NULL) { wayl_win_destroy(term->window); term->window = NULL; } mtx_lock(&term->render.workers.lock); xassert(tll_length(term->render.workers.queue) == 0); /* Count livinig threads - we may get here when only some of the * threads have been successfully started */ size_t worker_count = 0; if (term->render.workers.threads != NULL) { for (size_t i = 0; i < term->render.workers.count; i++, worker_count++) { if (term->render.workers.threads[i] == 0) break; } for (size_t i = 0; i < worker_count; i++) { sem_post(&term->render.workers.start); tll_push_back(term->render.workers.queue, -2); } } mtx_unlock(&term->render.workers.lock); key_binding_unref(term->wl->key_binding_manager, term->conf); urls_reset(term); free(term->vt.osc.data); free(term->vt.osc8.uri); composed_free(term->composed); free(term->app_id); free(term->window_title); tll_free_and_free(term->window_title_stack, free); for (size_t i = 0; i < sizeof(term->fonts) / sizeof(term->fonts[0]); i++) fcft_destroy(term->fonts[i]); for (size_t i = 0; i < 4; i++) free(term->font_sizes[i]); free_custom_glyphs( &term->custom_glyphs.box_drawing, GLYPH_BOX_DRAWING_COUNT); free_custom_glyphs( &term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT); free_custom_glyphs( &term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); free_custom_glyphs( &term->custom_glyphs.octants, GLYPH_OCTANTS_COUNT); free(term->search.buf); free(term->search.last.buf); if (term->render.workers.threads != NULL) { for (size_t i = 0; i < term->render.workers.count; i++) { if (term->render.workers.threads[i] != 0) thrd_join(term->render.workers.threads[i], NULL); } } free(term->render.workers.threads); mtx_destroy(&term->render.workers.lock); sem_destroy(&term->render.workers.start); sem_destroy(&term->render.workers.done); xassert(tll_length(term->render.workers.queue) == 0); tll_free(term->render.workers.queue); shm_unref(term->render.last_buf); shm_chain_free(term->render.chains.grid); shm_chain_free(term->render.chains.search); shm_chain_free(term->render.chains.scrollback_indicator); shm_chain_free(term->render.chains.render_timer); shm_chain_free(term->render.chains.url); shm_chain_free(term->render.chains.csd); shm_chain_free(term->render.chains.overlay); pixman_region32_fini(&term->render.last_overlay_clip); tll_free(term->tab_stops); tll_foreach(term->ptmx_buffers, it) { free(it->item.data); tll_remove(term->ptmx_buffers, it); } tll_foreach(term->ptmx_paste_buffers, it) { free(it->item.data); tll_remove(term->ptmx_paste_buffers, it); } notify_free(term, &term->kitty_notification); tll_foreach(term->active_notifications, it) { notify_free(term, &it->item); tll_remove(term->active_notifications, it); } for (size_t i = 0; i < ALEN(term->notification_icons); i++) notify_icon_free(&term->notification_icons[i]); sixel_fini(term); term_ime_reset(term); grid_free(&term->normal); grid_free(&term->alt); grid_free(term->interactive_resizing.grid); free(term->interactive_resizing.grid); free(term->foot_exe); free(term->cwd); free(term->mouse_user_cursor); free(term->color_stack.stack); int ret = EXIT_SUCCESS; if (term->slave > 0) { /* We'll deal with this explicitly */ reaper_del(term->reaper, term->slave); int exit_status; if (term->shutdown.client_has_terminated) exit_status = term->shutdown.exit_status; else { LOG_DBG("initiating blocking terminate of slave; " "sending SIGHUP to PID=%u", term->slave); kill(-term->slave, SIGHUP); /* * we've closed the ptxm, and sent SIGTERM to the client * application. It *should* exit... * * But, since it is possible to write clients that ignore * this, we need to handle it in *some* way. * * So, what we do is register a SIGALRM handler, and configure a 30 * second alarm. If the slave hasn't died after this time, we send * it a SIGKILL, * * Note that this solution is *not* asynchronous, and any * other events etc will be ignored during this time. This of * course only applies to a 'foot --server' instance, where * there might be other terminals running. */ struct sigaction action = {.sa_handler = &sig_alarm}; sigemptyset(&action.sa_mask); sigaction(SIGALRM, &action, NULL); /* Wait, then send SIGTERM, wait again, then send SIGKILL */ int next_signal = SIGTERM; alarm_raised = 0; alarm(30); while (true) { int r = waitpid(term->slave, &exit_status, 0); if (r == term->slave) break; if (r == -1) { xassert(errno == EINTR); if (alarm_raised) { LOG_DBG("slave (PID=%u) has not terminated yet, " "sending: %s (%d)", term->slave, next_signal == SIGTERM ? "SIGTERM" : "SIGKILL", next_signal); kill(-term->slave, next_signal); next_signal = SIGKILL; alarm_raised = 0; alarm(30); } } } /* Cancel alarm */ alarm(0); action.sa_handler = SIG_DFL; sigaction(SIGALRM, &action, NULL); } ret = EXIT_FAILURE; if (WIFEXITED(exit_status)) { ret = WEXITSTATUS(exit_status); LOG_DBG("slave exited with code %d", ret); } else if (WIFSIGNALED(exit_status)) { ret = WTERMSIG(exit_status); LOG_WARN("slave exited with signal %d (%s)", ret, strsignal(ret)); } else { LOG_WARN("slave exited for unknown reason (status = 0x%08x)", exit_status); } } free(term); #if defined(__GLIBC__) if (!malloc_trim(0)) LOG_WARN("failed to trim memory"); #endif return ret; } static inline void erase_cell_range(struct terminal *term, struct row *row, int start, int end) { xassert(start < term->cols); xassert(end < term->cols); row->dirty = true; const enum color_source bg_src = term->vt.attrs.bg_src; if (unlikely(bg_src != COLOR_DEFAULT)) { for (int col = start; col <= end; col++) { struct cell *c = &row->cells[col]; c->wc = 0; c->attrs = (struct attributes){.bg_src = bg_src, .bg = term->vt.attrs.bg}; } } else memset(&row->cells[start], 0, (end - start + 1) * sizeof(row->cells[0])); if (unlikely(row->extra != NULL)) { grid_row_uri_range_erase(row, start, end); grid_row_underline_range_erase(row, start, end); } } static inline void erase_line(struct terminal *term, struct row *row) { erase_cell_range(term, row, 0, term->cols - 1); row->linebreak = true; row->shell_integration.prompt_marker = false; row->shell_integration.cmd_start = -1; row->shell_integration.cmd_end = -1; } void term_reset(struct terminal *term, bool hard) { LOG_INFO("%s resetting the terminal", hard ? "hard" : "soft"); term->cursor_keys_mode = CURSOR_KEYS_NORMAL; term->keypad_keys_mode = KEYPAD_NUMERICAL; term->reverse = false; term->hide_cursor = false; term->reverse_wrap = true; term->auto_margin = true; term->insert_mode = false; term->bracketed_paste = false; term->focus_events = false; term->num_lock_modifier = true; term->bell_action_enabled = true; term->mouse_tracking = MOUSE_NONE; term->mouse_reporting = MOUSE_NORMAL; term->charsets.selected = G0; term->charsets.set[G0] = CHARSET_ASCII; term->charsets.set[G1] = CHARSET_ASCII; term->charsets.set[G2] = CHARSET_ASCII; term->charsets.set[G3] = CHARSET_ASCII; term->saved_charsets = term->charsets; tll_free_and_free(term->window_title_stack, free); term_set_window_title(term, term->conf->title); term_set_app_id(term, NULL); term_set_user_mouse_cursor(term, NULL); term->modify_other_keys_2 = false; memset(term->normal.kitty_kbd.flags, 0, sizeof(term->normal.kitty_kbd.flags)); memset(term->alt.kitty_kbd.flags, 0, sizeof(term->alt.kitty_kbd.flags)); term->normal.kitty_kbd.idx = term->alt.kitty_kbd.idx = 0; term->scroll_region.start = 0; term->scroll_region.end = term->rows; free(term->vt.osc8.uri); free(term->vt.osc.data); term->vt = (struct vt){ .state = 0, /* STATE_GROUND */ }; if (term->grid == &term->alt) { term->grid = &term->normal; selection_cancel(term); } term->meta.esc_prefix = true; term->meta.eight_bit = true; tll_foreach(term->normal.sixel_images, it) { sixel_destroy(&it->item); tll_remove(term->normal.sixel_images, it); } tll_foreach(term->alt.sixel_images, it) { sixel_destroy(&it->item); tll_remove(term->alt.sixel_images, it); } notify_free(term, &term->kitty_notification); tll_foreach(term->active_notifications, it) { notify_free(term, &it->item); tll_remove(term->active_notifications, it); } for (size_t i = 0; i < ALEN(term->notification_icons); i++) notify_icon_free(&term->notification_icons[i]); term->grapheme_shaping = term->conf->tweak.grapheme_shaping; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED term_ime_enable(term); #endif term->bits_affecting_ascii_printer.value = 0; term_update_ascii_printer(term); if (!hard) return; 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.cursor_fg = term->conf->cursor.color.text; term->colors.cursor_bg = term->conf->cursor.color.cursor; 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)); free(term->color_stack.stack); term->color_stack.stack = NULL; term->color_stack.size = 0; term->color_stack.idx = 0; term->origin = ORIGIN_ABSOLUTE; term->normal.cursor.lcf = false; term->alt.cursor.lcf = false; term->normal.cursor = (struct cursor){.point = {0, 0}}; term->normal.saved_cursor = (struct cursor){.point = {0, 0}}; term->alt.cursor = (struct cursor){.point = {0, 0}}; term->alt.saved_cursor = (struct cursor){.point = {0, 0}}; term->cursor_style = term->conf->cursor.style; term->cursor_blink.decset = false; term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; term_cursor_blink_update(term); selection_cancel(term); term->normal.offset = term->normal.view = 0; term->alt.offset = term->alt.view = 0; for (size_t i = 0; i < term->rows; i++) { struct row *r = grid_row_and_alloc(&term->normal, i); erase_line(term, r); } for (size_t i = 0; i < term->rows; i++) { struct row *r = grid_row_and_alloc(&term->alt, i); erase_line(term, r); } for (size_t i = term->rows; i < term->normal.num_rows; i++) { grid_row_free(term->normal.rows[i]); term->normal.rows[i] = NULL; } for (size_t i = term->rows; i < term->alt.num_rows; i++) { grid_row_free(term->alt.rows[i]); term->alt.rows[i] = NULL; } term->normal.cur_row = term->normal.rows[0]; term->alt.cur_row = term->alt.rows[0]; tll_free(term->normal.scroll_damage); tll_free(term->alt.scroll_damage); term->render.last_cursor.row = NULL; term_damage_all(term); term->sixel.scrolling = true; term->sixel.cursor_right_of_graphics = false; term->sixel.use_private_palette = true; term->sixel.max_width = SIXEL_MAX_WIDTH; term->sixel.max_height = SIXEL_MAX_HEIGHT; term->sixel.palette_size = SIXEL_MAX_COLORS; free(term->sixel.private_palette); free(term->sixel.shared_palette); term->sixel.private_palette = term->sixel.shared_palette = NULL; } static bool term_font_size_adjust_by_points(struct terminal *term, float amount) { const struct config *conf = term->conf; const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; for (size_t i = 0; i < 4; i++) { const struct config_font_list *font_list = &conf->fonts[i]; for (size_t j = 0; j < font_list->count; j++) { struct config_font *font = &term->font_sizes[i][j]; float old_pt_size = font->pt_size; if (font->px_size > 0) old_pt_size = font->px_size * 72. / dpi; font->pt_size = fmaxf(old_pt_size + amount, 0.); font->px_size = -1; } } return reload_fonts(term, true); } static bool term_font_size_adjust_by_pixels(struct terminal *term, int amount) { const struct config *conf = term->conf; const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; for (size_t i = 0; i < 4; i++) { const struct config_font_list *font_list = &conf->fonts[i]; for (size_t j = 0; j < font_list->count; j++) { struct config_font *font = &term->font_sizes[i][j]; int old_px_size = font->px_size; if (font->px_size <= 0) old_px_size = font->pt_size * dpi / 72.; font->px_size = max(old_px_size + amount, 1); } } return reload_fonts(term, true); } static bool term_font_size_adjust_by_percent(struct terminal *term, bool increment, float percent) { const struct config *conf = term->conf; const float multiplier = increment ? 1. + percent : 1. / (1. + percent); for (size_t i = 0; i < 4; i++) { const struct config_font_list *font_list = &conf->fonts[i]; for (size_t j = 0; j < font_list->count; j++) { struct config_font *font = &term->font_sizes[i][j]; if (font->px_size > 0) font->px_size = max(font->px_size * multiplier, 1); else font->pt_size = fmax(font->pt_size * multiplier, 0); } } return reload_fonts(term, true); } bool term_font_size_increase(struct terminal *term) { const struct config *conf = term->conf; const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment; if (inc_dec->percent > 0.) return term_font_size_adjust_by_percent(term, true, inc_dec->percent); else if (inc_dec->pt_or_px.px > 0) return term_font_size_adjust_by_pixels(term, inc_dec->pt_or_px.px); else return term_font_size_adjust_by_points(term, inc_dec->pt_or_px.pt); } bool term_font_size_decrease(struct terminal *term) { const struct config *conf = term->conf; const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment; if (inc_dec->percent > 0.) return term_font_size_adjust_by_percent(term, false, inc_dec->percent); else if (inc_dec->pt_or_px.px > 0) return term_font_size_adjust_by_pixels(term, -inc_dec->pt_or_px.px); else return term_font_size_adjust_by_points(term, -inc_dec->pt_or_px.pt); } bool term_font_size_reset(struct terminal *term) { return load_fonts_from_conf(term); } bool term_fractional_scaling(const struct terminal *term) { return term->wl->fractional_scale_manager != NULL && term->wl->viewporter != NULL && term->window->scale > 0.; } bool term_preferred_buffer_scale(const struct terminal *term) { return term->window->preferred_buffer_scale > 0; } bool term_update_scale(struct terminal *term) { const struct wl_window *win = term->window; /* * We have a number of "sources" we can use as scale. We choose * the scale in the following order: * * - "preferred" scale, from the fractional-scale-v1 protocol * - "preferred" scale, from wl_compositor version 6. NOTE: if the compositor advertises version 6 we must use 1.0 until wl_surface.preferred_buffer_scale is sent * - scaling factor of output we most recently were mapped on * - if we're not mapped, use the last known scaling factor * - if we're not mapped, and we don't have a last known scaling * factor, use the scaling factor from the first available * output. * - if there aren't any outputs available, use 1.0 */ const float new_scale = (term_fractional_scaling(term) ? win->scale : term_preferred_buffer_scale(term) ? win->preferred_buffer_scale : tll_length(win->on_outputs) > 0 ? tll_back(win->on_outputs)->scale : term->scale_before_unmap > 0. ? term->scale_before_unmap : tll_length(term->wl->monitors) > 0 ? tll_front(term->wl->monitors).scale : 1.); if (new_scale == term->scale) return false; LOG_DBG("scaling factor changed: %.2f -> %.2f", term->scale, new_scale); term->scale_before_unmap = new_scale; term->scale = new_scale; return true; } bool term_font_dpi_changed(struct terminal *term, float old_scale) { float dpi = get_font_dpi(term); xassert(term->scale > 0.); bool was_scaled_using_dpi = term->font_is_sized_by_dpi; bool will_scale_using_dpi = term->conf->dpi_aware; bool need_font_reload = was_scaled_using_dpi != will_scale_using_dpi || (will_scale_using_dpi ? term->font_dpi != dpi : old_scale != term->scale); if (need_font_reload) { LOG_DBG("DPI/scale change: DPI-aware=%s, " "DPI: %.2f -> %.2f, scale: %.2f -> %.2f, " "sizing font based on monitor's %s", term->conf->dpi_aware ? "yes" : "no", term->font_dpi, dpi, old_scale, term->scale, will_scale_using_dpi ? "DPI" : "scaling factor"); } term->font_dpi = dpi; term->font_dpi_before_unmap = dpi; term->font_is_sized_by_dpi = will_scale_using_dpi; if (!need_font_reload) return false; return reload_fonts(term, false); } void term_font_subpixel_changed(struct terminal *term) { enum fcft_subpixel subpixel = get_font_subpixel(term); if (term->font_subpixel == subpixel) return; #if defined(_DEBUG) && LOG_ENABLE_DBG static const char *const str[] = { [FCFT_SUBPIXEL_DEFAULT] = "default", [FCFT_SUBPIXEL_NONE] = "disabled", [FCFT_SUBPIXEL_HORIZONTAL_RGB] = "RGB", [FCFT_SUBPIXEL_HORIZONTAL_BGR] = "BGR", [FCFT_SUBPIXEL_VERTICAL_RGB] = "V-RGB", [FCFT_SUBPIXEL_VERTICAL_BGR] = "V-BGR", }; LOG_DBG("subpixel mode changed: %s -> %s", str[term->font_subpixel], str[subpixel]); #endif term->font_subpixel = subpixel; term_damage_view(term); render_refresh(term); } int term_font_baseline(const struct terminal *term) { const struct fcft_font *font = term->fonts[0]; const int line_height = term->cell_height; const int font_height = font->ascent + font->descent; /* * Center glyph on the line *if* using a custom line height, * otherwise the baseline is simply 'descent' pixels above the * bottom of the cell */ const int glyph_top_y = term->font_line_height.px >= 0 ? round((line_height - font_height) / 2.) : 0; return term->font_y_ofs + line_height - glyph_top_y - font->descent; } void term_damage_rows(struct terminal *term, int start, int end) { xassert(start <= end); for (int r = start; r <= end; r++) { struct row *row = grid_row(term->grid, r); row->dirty = true; for (int c = 0; c < term->grid->num_cols; c++) row->cells[c].attrs.clean = 0; } } void term_damage_rows_in_view(struct terminal *term, int start, int end) { xassert(start <= end); for (int r = start; r <= end; r++) { struct row *row = grid_row_in_view(term->grid, r); row->dirty = true; for (int c = 0; c < term->grid->num_cols; c++) row->cells[c].attrs.clean = 0; } } void term_damage_all(struct terminal *term) { term_damage_rows(term, 0, term->rows - 1); } void term_damage_view(struct terminal *term) { term_damage_rows_in_view(term, 0, term->rows - 1); } void term_damage_cursor(struct terminal *term) { term->grid->cur_row->cells[term->grid->cursor.point.col].attrs.clean = 0; term->grid->cur_row->dirty = true; } void term_damage_margins(struct terminal *term) { term->render.margins = true; } void term_damage_color(struct terminal *term, enum color_source src, int idx) { xassert(src == COLOR_DEFAULT || src == COLOR_BASE256); for (int r = 0; r < term->rows; r++) { struct row *row = grid_row_in_view(term->grid, r); struct cell *cell = &row->cells[0]; const struct cell *end = &row->cells[term->cols]; for (; cell < end; cell++) { bool dirty = false; switch (cell->attrs.fg_src) { case COLOR_BASE16: case COLOR_BASE256: if (src == COLOR_BASE256 && cell->attrs.fg == idx) dirty = true; break; case COLOR_DEFAULT: if (src == COLOR_DEFAULT) { /* Doesn't matter whether we've updated the default foreground, or background, we still want to dirty this cell, to be sure we handle all cases of color inversion/reversal */ dirty = true; } break; case COLOR_RGB: /* Not affected */ break; } switch (cell->attrs.bg_src) { case COLOR_BASE16: case COLOR_BASE256: if (src == COLOR_BASE256 && cell->attrs.bg == idx) dirty = true; break; case COLOR_DEFAULT: if (src == COLOR_DEFAULT) { /* Doesn't matter whether we've updated the default foreground, or background, we still want to dirty this cell, to be sure we handle all cases of color inversion/reversal */ dirty = true; } break; case COLOR_RGB: /* Not affected */ break; } if (dirty) { cell->attrs.clean = 0; row->dirty = true; } } /* Colored underlines */ if (row->extra != NULL) { const struct row_ranges *underlines = &row->extra->underline_ranges; for (int i = 0; i < underlines->count; i++) { const struct row_range *range = &underlines->v[i]; /* Underline colors are either default, or BASE256/RGB, but never BASE16 */ xassert(range->underline.color_src == COLOR_DEFAULT || range->underline.color_src == COLOR_BASE256 || range->underline.color_src == COLOR_RGB); if (range->underline.color_src == src) { struct cell *c = &row->cells[range->start]; const struct cell *e = &row->cells[range->end + 1]; for (; c < e; c++) c->attrs.clean = 0; row->dirty = true; } } } } } void term_damage_scroll(struct terminal *term, enum damage_type damage_type, struct scroll_region region, int lines) { if (likely(tll_length(term->grid->scroll_damage) > 0)) { struct damage *dmg = &tll_back(term->grid->scroll_damage); if (likely( dmg->type == damage_type && dmg->region.start == region.start && dmg->region.end == region.end)) { /* Make sure we don't overflow... */ int new_line_count = (int)dmg->lines + lines; if (likely(new_line_count <= UINT16_MAX)) { dmg->lines = new_line_count; return; } } } struct damage dmg = { .type = damage_type, .region = region, .lines = lines, }; tll_push_back(term->grid->scroll_damage, dmg); } void term_erase(struct terminal *term, int start_row, int start_col, int end_row, int end_col) { xassert(start_row <= end_row); xassert(start_col <= end_col || start_row < end_row); if (start_row == end_row) { struct row *row = grid_row(term->grid, start_row); erase_cell_range(term, row, start_col, end_col); sixel_overwrite_by_row(term, start_row, start_col, end_col - start_col + 1); return; } xassert(end_row > start_row); erase_cell_range( term, grid_row(term->grid, start_row), start_col, term->cols - 1); sixel_overwrite_by_row(term, start_row, start_col, term->cols - start_col); for (int r = start_row + 1; r < end_row; r++) erase_line(term, grid_row(term->grid, r)); sixel_overwrite_by_rectangle( term, start_row + 1, 0, end_row - start_row, term->cols); erase_cell_range(term, grid_row(term->grid, end_row), 0, end_col); sixel_overwrite_by_row(term, end_row, 0, end_col + 1); } void term_erase_scrollback(struct terminal *term) { const struct grid *grid = term->grid; const int num_rows = grid->num_rows; const int mask = num_rows - 1; const int scrollback_history_size = num_rows - term->rows; if (scrollback_history_size == 0) return; const int start = (grid->offset + term->rows) & mask; const int end = (grid->offset - 1) & mask; const int rel_start = grid_row_abs_to_sb(grid, term->rows, start); const int rel_end = grid_row_abs_to_sb(grid, term->rows, end); const int sel_start = selection_get_start(term).row; const int sel_end = selection_get_end(term).row; if (sel_end >= 0) { /* * Cancel selection if it touches any of the rows in the * scrollback, since we can't have the selection reference * soon-to-be deleted rows. * * This is done by range checking the selection range against * the scrollback range. * * To make this comparison simpler, the start/end absolute row * numbers are "rebased" against the scrollback start, where * row 0 is the *first* row in the scrollback. A high number * thus means the row is further *down* in the scrollback, * closer to the screen bottom. */ const int rel_sel_start = grid_row_abs_to_sb(grid, term->rows, sel_start); const int rel_sel_end = grid_row_abs_to_sb(grid, term->rows, sel_end); if ((rel_sel_start <= rel_start && rel_sel_end >= rel_start) || (rel_sel_start <= rel_end && rel_sel_end >= rel_end) || (rel_sel_start >= rel_start && rel_sel_end <= rel_end)) { selection_cancel(term); } } tll_foreach(term->grid->sixel_images, it) { struct sixel *six = &it->item; const int six_start = grid_row_abs_to_sb(grid, term->rows, six->pos.row); const int six_end = grid_row_abs_to_sb( grid, term->rows, six->pos.row + six->rows - 1); if ((six_start <= rel_start && six_end >= rel_start) || (six_start <= rel_end && six_end >= rel_end) || (six_start >= rel_start && six_end <= rel_end)) { sixel_destroy(six); tll_remove(term->grid->sixel_images, it); } } for (int i = start;; i = (i + 1) & mask) { struct row *row = term->grid->rows[i]; if (row != NULL) { if (term->render.last_cursor.row == row) term->render.last_cursor.row = NULL; grid_row_free(row); term->grid->rows[i] = NULL; } if (i == end) break; } term->grid->view = term->grid->offset; #if defined(_DEBUG) for (int i = 0; i < term->rows; i++) { xassert(grid_row_in_view(term->grid, i) != NULL); } #endif term_damage_view(term); } UNITTEST { const int scrollback_rows = 16; const int term_rows = 5; const int cols = 5; struct fdm *fdm = fdm_init(); xassert(fdm != NULL); struct terminal term = { .fdm = fdm, .rows = term_rows, .cols = cols, .normal = { .rows = xcalloc(scrollback_rows, sizeof(term.normal.rows[0])), .num_rows = scrollback_rows, .num_cols = cols, }, .grid = &term.normal, .selection = { .coords = { .start = {-1, -1}, .end = {-1, -1}, }, .kind = SELECTION_NONE, .auto_scroll = { .fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK), }, }, }; 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) { \ struct row *r = xcalloc(1, sizeof(*term.normal.rows[i])); \ r->cells = xcalloc(cols, sizeof(r->cells[0])); \ term.normal.rows[i] = r; \ } \ } \ } while (0) /* * Test case 1 - no selection, just verify all rows except those * on screen have been deleted. */ populate_scrollback(); term.normal.offset = 11; term_erase_scrollback(&term); for (int i = 0; i < scrollback_rows; i++) { if (i >= term.normal.offset && i < term.normal.offset + term_rows) xassert(term.normal.rows[i] != NULL); else xassert(term.normal.rows[i] == NULL); } /* * Test case 2 - selection that touches the scrollback. Verify the * selection is cancelled. */ term.normal.offset = 14; /* Screen covers rows 14,15,0,1,2 */ /* Selection covers rows 15,0,1,2,3 */ term.selection.coords.start = (struct coord){.row = 15}; term.selection.coords.end = (struct coord){.row = 19}; term.selection.kind = SELECTION_CHAR_WISE; populate_scrollback(); term_erase_scrollback(&term); xassert(term.selection.coords.start.row < 0); xassert(term.selection.coords.end.row < 0); xassert(term.selection.kind == SELECTION_NONE); /* * Test case 3 - selection that does *not* touch the * scrollback. Verify the selection is *not* cancelled. */ /* Selection covers rows 15,0 */ term.selection.coords.start = (struct coord){.row = 15}; term.selection.coords.end = (struct coord){.row = 16}; term.selection.kind = SELECTION_CHAR_WISE; populate_scrollback(); term_erase_scrollback(&term); xassert(term.selection.coords.start.row == 15); xassert(term.selection.coords.end.row == 16); xassert(term.selection.kind == SELECTION_CHAR_WISE); term.selection.coords.start = (struct coord){-1, -1}; term.selection.coords.end = (struct coord){-1, -1}; term.selection.kind = SELECTION_NONE; /* * Test case 4 - sixel that touch the scrollback */ struct sixel six = { .rows = 5, .pos = { .row = 15, }, }; tll_push_back(term.normal.sixel_images, six); populate_scrollback(); term_erase_scrollback(&term); xassert(tll_length(term.normal.sixel_images) == 0); /* * Test case 5 - sixel that does *not* touch the scrollback */ six.rows = 3; tll_push_back(term.normal.sixel_images, six); populate_scrollback(); term_erase_scrollback(&term); xassert(tll_length(term.normal.sixel_images) == 1); /* Cleanup */ tll_free(term.normal.sixel_images); close(term.selection.auto_scroll.fd); for (int i = 0; i < scrollback_rows; i++) grid_row_free(term.normal.rows[i]); free(term.normal.rows); fdm_destroy(fdm); } int term_row_rel_to_abs(const struct terminal *term, int row) { switch (term->origin) { case ORIGIN_ABSOLUTE: return min(row, term->rows - 1); case ORIGIN_RELATIVE: return min(row + term->scroll_region.start, term->scroll_region.end - 1); } BUG("Invalid cursor_origin value"); return -1; } void term_cursor_to(struct terminal *term, int row, int col) { xassert(row < term->rows); xassert(col < term->cols); term->grid->cursor.lcf = false; term->grid->cursor.point.col = col; term->grid->cursor.point.row = row; term_reset_grapheme_state(term); term->grid->cur_row = grid_row(term->grid, row); } void term_cursor_home(struct terminal *term) { term_cursor_to(term, term_row_rel_to_abs(term, 0), 0); } void term_cursor_col(struct terminal *term, int col) { xassert(col < term->cols); term->grid->cursor.lcf = false; term->grid->cursor.point.col = col; term_reset_grapheme_state(term); } void term_cursor_left(struct terminal *term, int count) { int move_amount = min(term->grid->cursor.point.col, count); term->grid->cursor.point.col -= move_amount; xassert(term->grid->cursor.point.col >= 0); term->grid->cursor.lcf = false; term_reset_grapheme_state(term); } void term_cursor_right(struct terminal *term, int count) { int move_amount = min(term->cols - term->grid->cursor.point.col - 1, count); term->grid->cursor.point.col += move_amount; xassert(term->grid->cursor.point.col < term->cols); term->grid->cursor.lcf = false; term_reset_grapheme_state(term); } void term_cursor_up(struct terminal *term, int count) { int top = term->origin == ORIGIN_ABSOLUTE ? 0 : term->scroll_region.start; xassert(term->grid->cursor.point.row >= top); int move_amount = min(term->grid->cursor.point.row - top, count); term_cursor_to(term, term->grid->cursor.point.row - move_amount, term->grid->cursor.point.col); } void term_cursor_down(struct terminal *term, int count) { int bottom = term->origin == ORIGIN_ABSOLUTE ? term->rows : term->scroll_region.end; xassert(bottom >= term->grid->cursor.point.row); int move_amount = min(bottom - term->grid->cursor.point.row - 1, count); term_cursor_to(term, term->grid->cursor.point.row + move_amount, term->grid->cursor.point.col); } static bool cursor_blink_rearm_timer(struct terminal *term) { if (term->cursor_blink.fd < 0) { int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (fd < 0) { LOG_ERRNO("failed to create cursor blink timer FD"); return false; } if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_cursor_blink, term)) { close(fd); return false; } term->cursor_blink.fd = fd; } const int rate_ms = term->conf->cursor.blink.rate_ms; const long secs = rate_ms / 1000; const long nsecs = (rate_ms % 1000) * 1000000; const struct itimerspec timer = { .it_value = {.tv_sec = secs, .tv_nsec = nsecs}, .it_interval = {.tv_sec = secs, .tv_nsec = nsecs}, }; if (timerfd_settime(term->cursor_blink.fd, 0, &timer, NULL) < 0) { LOG_ERRNO("failed to arm cursor blink timer"); fdm_del(term->fdm, term->cursor_blink.fd); term->cursor_blink.fd = -1; return false; } return true; } static bool cursor_blink_disarm_timer(struct terminal *term) { fdm_del(term->fdm, term->cursor_blink.fd); term->cursor_blink.fd = -1; return true; } void term_cursor_blink_update(struct terminal *term) { bool enable = term->cursor_blink.decset || term->cursor_blink.deccsusr; bool activate = !term->shutdown.in_progress && enable && term->visual_focus; LOG_DBG("decset=%d, deccsrusr=%d, focus=%d, shutting-down=%d, enable=%d, activate=%d", term->cursor_blink.decset, term->cursor_blink.deccsusr, term->visual_focus, term->shutdown.in_progress, enable, activate); if (activate && term->cursor_blink.fd < 0) { term->cursor_blink.state = CURSOR_BLINK_ON; cursor_blink_rearm_timer(term); } else if (!activate && term->cursor_blink.fd >= 0) cursor_blink_disarm_timer(term); } static bool selection_on_top_region(const struct terminal *term, struct scroll_region region) { return region.start > 0 && selection_on_rows(term, 0, region.start - 1); } static bool selection_on_bottom_region(const struct terminal *term, struct scroll_region region) { return region.end < term->rows && selection_on_rows(term, region.end, term->rows - 1); } void term_scroll_partial(struct terminal *term, struct scroll_region region, int rows) { LOG_DBG("scroll: rows=%d, region.start=%d, region.end=%d", rows, region.start, region.end); /* Verify scroll amount has been clamped */ xassert(rows <= region.end - region.start); /* Cancel selections that cannot be scrolled */ if (unlikely(term->selection.coords.end.row >= 0)) { /* * Selection is (partly) inside either the top or bottom * scrolling regions, or on (at least one) of the lines * scrolled in (i.e. reused lines). */ if (selection_on_top_region(term, region) || selection_on_bottom_region(term, region)) { selection_cancel(term); } else selection_scroll_up(term, rows); } sixel_scroll_up(term, rows); /* How many lines from the scrollback start is the current viewport? */ int view_sb_start_distance = grid_row_abs_to_sb( term->grid, term->rows, term->grid->view); bool view_follows = term->grid->view == term->grid->offset; term->grid->offset += rows; term->grid->offset &= term->grid->num_rows - 1; if (likely(view_follows)) { term_damage_scroll(term, DAMAGE_SCROLL, region, rows); selection_view_down(term, term->grid->offset); term->grid->view = term->grid->offset; } else if (unlikely(rows > view_sb_start_distance)) { /* Part of current view is being scrolled out */ int new_view = grid_row_sb_to_abs(term->grid, term->rows, 0); selection_view_down(term, new_view); cmd_scrollback_down(term, rows - view_sb_start_distance); } /* Top non-scrolling region. */ for (int i = region.start - 1; i >= 0; i--) grid_swap_row(term->grid, i - rows, i); /* Bottom non-scrolling region */ for (int i = term->rows - 1; i >= region.end; i--) grid_swap_row(term->grid, i - rows, i); /* Erase scrolled in lines */ for (int r = region.end - rows; r < region.end; r++) { struct row *row = grid_row_and_alloc(term->grid, r); erase_line(term, row); } term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); #if defined(_DEBUG) for (int r = 0; r < term->rows; r++) xassert(grid_row(term->grid, r) != NULL); #endif } void term_scroll(struct terminal *term, int rows) { term_scroll_partial(term, term->scroll_region, rows); } void term_scroll_reverse_partial(struct terminal *term, struct scroll_region region, int rows) { LOG_DBG("scroll reverse: rows=%d, region.start=%d, region.end=%d", rows, region.start, region.end); /* Verify scroll amount has been clamped */ xassert(rows <= region.end - region.start); /* Cancel selections that cannot be scrolled */ if (unlikely(term->selection.coords.end.row >= 0)) { /* * Selection is (partly) inside either the top or bottom * scrolling regions, or on (at least one) of the lines * scrolled in (i.e. reused lines). */ if (selection_on_top_region(term, region) || selection_on_bottom_region(term, region)) { selection_cancel(term); } else selection_scroll_down(term, rows); } /* Unallocate scrolled out lines */ for (int r = region.end - rows; r < region.end; r++) { const int abs_r = grid_row_absolute(term->grid, r); struct row *row = term->grid->rows[abs_r]; grid_row_free(row); term->grid->rows[abs_r] = NULL; if (term->render.last_cursor.row == row) term->render.last_cursor.row = NULL; } sixel_scroll_down(term, rows); 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; 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; } /* Bottom non-scrolling region */ for (int i = region.end + rows; i < term->rows + rows; i++) grid_swap_row(term->grid, i, i - rows); /* Top non-scrolling region */ for (int i = 0 + rows; i < region.start + rows; i++) grid_swap_row(term->grid, i, i - rows); /* Erase scrolled in lines */ for (int r = region.start; r < region.start + rows; r++) { struct row *row = grid_row_and_alloc(term->grid, r); erase_line(term, row); } term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); #if defined(_DEBUG) for (int r = 0; r < term->rows; r++) xassert(grid_row(term->grid, r) != NULL); #endif } void term_scroll_reverse(struct terminal *term, int rows) { term_scroll_reverse_partial(term, term->scroll_region, rows); } void term_carriage_return(struct terminal *term) { term_cursor_left(term, term->grid->cursor.point.col); } void term_linefeed(struct terminal *term) { term->grid->cursor.lcf = false; if (term->grid->cursor.point.row == term->scroll_region.end - 1) term_scroll(term, 1); else term_cursor_down(term, 1); term_reset_grapheme_state(term); } void term_reverse_index(struct terminal *term) { if (term->grid->cursor.point.row == term->scroll_region.start) term_scroll_reverse(term, 1); else term_cursor_up(term, 1); } void term_reset_view(struct terminal *term) { if (term->grid->view == term->grid->offset) return; term->grid->view = term->grid->offset; term_damage_view(term); } void term_save_cursor(struct terminal *term) { term->grid->saved_cursor = term->grid->cursor; term->vt.saved_attrs = term->vt.attrs; term->saved_charsets = term->charsets; } void term_restore_cursor(struct terminal *term, const struct cursor *cursor) { int row = min(cursor->point.row, term->rows - 1); int col = min(cursor->point.col, term->cols - 1); term_cursor_to(term, row, col); term->grid->cursor.lcf = cursor->lcf; term->vt.attrs = term->vt.saved_attrs; term->charsets = term->saved_charsets; term->bits_affecting_ascii_printer.charset = term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); } void term_visual_focus_in(struct terminal *term) { if (term->visual_focus) return; term->visual_focus = true; term_cursor_blink_update(term); render_refresh_csd(term); } void term_visual_focus_out(struct terminal *term) { if (!term->visual_focus) return; term->visual_focus = false; term_cursor_blink_update(term); render_refresh_csd(term); } void term_kbd_focus_in(struct terminal *term) { if (term->kbd_focus) return; term->kbd_focus = true; if (term->render.urgency) { term->render.urgency = false; term_damage_margins(term); } cursor_refresh(term); if (term->focus_events) term_to_slave(term, "\033[I", 3); } void term_kbd_focus_out(struct terminal *term) { if (!term->kbd_focus) return; tll_foreach(term->wl->seats, it) if (it->item.kbd_focus == term) return; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (term_ime_reset(term)) render_refresh(term); #endif term->kbd_focus = false; cursor_refresh(term); if (term->focus_events) term_to_slave(term, "\033[O", 3); } static int linux_mouse_button_to_x(int button) { /* Note: on X11, scroll events where reported as buttons. Not so * on Wayland. We manually map scroll events to custom "button" * defines (BTN_WHEEL_*). */ switch (button) { case BTN_LEFT: return 1; case BTN_MIDDLE: return 2; case BTN_RIGHT: return 3; case BTN_WHEEL_BACK: return 4; /* Foot custom define */ case BTN_WHEEL_FORWARD: return 5; /* Foot custom define */ case BTN_WHEEL_LEFT: return 6; /* Foot custom define */ case BTN_WHEEL_RIGHT: return 7; /* Foot custom define */ case BTN_SIDE: return 8; case BTN_EXTRA: return 9; case BTN_FORWARD: return 10; case BTN_BACK: return 11; case BTN_TASK: return 12; /* Guessing... */ default: LOG_WARN("unrecognized mouse button: %d (0x%x)", button, button); return -1; } } static int encode_xbutton(int xbutton) { switch (xbutton) { case 1: case 2: case 3: return xbutton - 1; case 4: case 5: case 6: case 7: /* Like button 1 and 2, but with 64 added */ return xbutton - 4 + 64; case 8: case 9: case 10: case 11: /* Similar to 4 and 5, but adding 128 instead of 64 */ return xbutton - 8 + 128; default: LOG_ERR("cannot encode X mouse button: %d", xbutton); return -1; } } static void report_mouse_click(struct terminal *term, int encoded_button, int row, int col, int row_pixels, int col_pixels, bool release) { char response[128]; switch (term->mouse_reporting) { case MOUSE_NORMAL: { int encoded_col = 32 + col + 1; int encoded_row = 32 + row + 1; if (encoded_col > 255 || encoded_row > 255) return; snprintf(response, sizeof(response), "\033[M%c%c%c", 32 + (release ? 3 : encoded_button), encoded_col, encoded_row); break; } case MOUSE_SGR: snprintf(response, sizeof(response), "\033[<%d;%d;%d%c", encoded_button, col + 1, row + 1, release ? 'm' : 'M'); break; case MOUSE_SGR_PIXELS: snprintf(response, sizeof(response), "\033[<%d;%d;%d%c", encoded_button, col_pixels + 1, row_pixels + 1, release ? 'm' : 'M'); break; case MOUSE_URXVT: snprintf(response, sizeof(response), "\033[%d;%d;%dM", 32 + (release ? 3 : encoded_button), col + 1, row + 1); break; case MOUSE_UTF8: /* Unimplemented */ return; } term_to_slave(term, response, strlen(response)); } static void report_mouse_motion(struct terminal *term, int encoded_button, int row, int col, int row_pixels, int col_pixels) { report_mouse_click(term, encoded_button, row, col, row_pixels, col_pixels, false); } bool term_mouse_grabbed(const struct terminal *term, const struct seat *seat) { /* * Mouse is grabbed by us, regardless of whether mouse tracking * has been enabled or not. */ xkb_mod_mask_t mods; get_current_modifiers(seat, &mods, NULL, 0, true); const struct key_binding_set *bindings = key_binding_for(term->wl->key_binding_manager, term->conf, seat); const xkb_mod_mask_t override_modmask = bindings->selection_overrides; bool override_mods_pressed = (mods & override_modmask) == override_modmask; return term->mouse_tracking == MOUSE_NONE || (seat->kbd_focus == term && override_mods_pressed); } void term_mouse_down(struct terminal *term, int button, int row, int col, int row_pixels, int col_pixels, bool _shift, bool _alt, bool _ctrl) { /* Map libevent button event code to X button number */ int xbutton = linux_mouse_button_to_x(button); if (xbutton == -1) return; int encoded = encode_xbutton(xbutton); if (encoded == -1) return; bool has_focus = term->kbd_focus; bool shift = has_focus ? _shift : false; bool alt = has_focus ? _alt : false; bool ctrl = has_focus ? _ctrl : false; encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0); switch (term->mouse_tracking) { case MOUSE_NONE: break; case MOUSE_CLICK: case MOUSE_DRAG: case MOUSE_MOTION: report_mouse_click(term, encoded, row, col, row_pixels, col_pixels, false); break; case MOUSE_X10: /* Never enabled */ BUG("X10 mouse mode not implemented"); break; } } void term_mouse_up(struct terminal *term, int button, int row, int col, int row_pixels, int col_pixels, bool _shift, bool _alt, bool _ctrl) { /* Map libevent button event code to X button number */ int xbutton = linux_mouse_button_to_x(button); if (xbutton == -1) return; if (xbutton == 4 || xbutton == 5) { /* No release events for vertical scroll wheel buttons */ return; } int encoded = encode_xbutton(xbutton); if (encoded == -1) return; bool has_focus = term->kbd_focus; bool shift = has_focus ? _shift : false; bool alt = has_focus ? _alt : false; bool ctrl = has_focus ? _ctrl : false; encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0); switch (term->mouse_tracking) { case MOUSE_NONE: break; case MOUSE_CLICK: case MOUSE_DRAG: case MOUSE_MOTION: report_mouse_click(term, encoded, row, col, row_pixels, col_pixels, true); break; case MOUSE_X10: /* Never enabled */ BUG("X10 mouse mode not implemented"); break; } } void term_mouse_motion(struct terminal *term, int button, int row, int col, int row_pixels, int col_pixels, bool _shift, bool _alt, bool _ctrl) { int encoded = 0; if (button != 0) { /* Map libevent button event code to X button number */ int xbutton = linux_mouse_button_to_x(button); if (xbutton == -1) return; encoded = encode_xbutton(xbutton); if (encoded == -1) return; } else encoded = 3; /* "released" */ bool has_focus = term->kbd_focus; bool shift = has_focus ? _shift : false; bool alt = has_focus ? _alt : false; bool ctrl = has_focus ? _ctrl : false; encoded += 32; /* Motion event */ encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0); switch (term->mouse_tracking) { case MOUSE_NONE: case MOUSE_CLICK: return; case MOUSE_DRAG: if (button == 0) return; /* FALLTHROUGH */ case MOUSE_MOTION: report_mouse_motion(term, encoded, row, col, row_pixels, col_pixels); break; case MOUSE_X10: /* Never enabled */ BUG("X10 mouse mode not implemented"); break; } } void term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) { enum cursor_shape shape = CURSOR_SHAPE_NONE; switch (term->active_surface) { case TERM_SURF_GRID: if (seat->pointer.hidden) shape = CURSOR_SHAPE_HIDDEN; else if (cursor_string_to_server_shape(term->mouse_user_cursor) != 0 || render_xcursor_is_valid(seat, term->mouse_user_cursor)) { shape = CURSOR_SHAPE_CUSTOM; } else if (term_mouse_grabbed(term, seat)) { shape = CURSOR_SHAPE_TEXT; } else shape = CURSOR_SHAPE_LEFT_PTR; break; case TERM_SURF_TITLE: case TERM_SURF_BUTTON_MINIMIZE: case TERM_SURF_BUTTON_MAXIMIZE: case TERM_SURF_BUTTON_CLOSE: shape = CURSOR_SHAPE_LEFT_PTR; break; case TERM_SURF_BORDER_LEFT: case TERM_SURF_BORDER_RIGHT: case TERM_SURF_BORDER_TOP: case TERM_SURF_BORDER_BOTTOM: shape = xcursor_for_csd_border(term, seat->mouse.x, seat->mouse.y); break; case TERM_SURF_NONE: return; } if (shape == CURSOR_SHAPE_NONE) BUG("xcursor not set"); render_xcursor_set(seat, term, shape); } void term_xcursor_update(struct terminal *term) { tll_foreach(term->wl->seats, it) term_xcursor_update_for_seat(term, &it->item); } void term_set_window_title(struct terminal *term, const char *title) { if (term->conf->locked_title && term->window_title_has_been_set) return; if (term->window_title != NULL && streq(term->window_title, title)) return; if (!is_valid_utf8_and_printable(title)) { /* It's an xdg_toplevel::set_title() protocol violation to set a title with an invalid UTF-8 sequence */ LOG_WARN("%s: title is not valid UTF-8, ignoring", title); return; } free(term->window_title); term->window_title = xstrdup(title); render_refresh_title(term); term->window_title_has_been_set = true; } void term_set_app_id(struct terminal *term, const char *app_id) { if (app_id != NULL && *app_id == '\0') app_id = NULL; if (term->app_id == NULL && app_id == NULL) return; if (term->app_id != NULL && app_id != NULL && streq(term->app_id, app_id)) return; if (app_id != NULL && !is_valid_utf8_and_printable(app_id)) { LOG_WARN("%s: app-id is not valid UTF-8, ignoring", app_id); return; } free(term->app_id); if (app_id != NULL) { term->app_id = xstrdup(app_id); } else { term->app_id = NULL; } const size_t length = app_id != NULL ? strlen(app_id) : 0; if (length > 2048) { /* * Not sure if there's a limit in the protocol, or the * libwayland implementation, or e.g. wlroots, but too long * app-id's (not e.g. title) causes at least river and sway to * peg the CPU at 100%, and stop sending e.g. frame callbacks. * */ term->app_id[2048] = '\0'; } render_refresh_app_id(term); render_refresh_icon(term); } const char * term_icon(const struct terminal *term) { const char *app_id = term->app_id != NULL ? term->app_id : term->conf->app_id; return #if 0 term->window_icon != NULL ? term->window_icon : #endif streq(app_id, "footclient") ? "foot" : app_id; } void term_flash(struct terminal *term, unsigned duration_ms) { LOG_DBG("FLASH for %ums", duration_ms); struct itimerspec alarm = { .it_value = {.tv_sec = 0, .tv_nsec = duration_ms * 1000000}, }; if (timerfd_settime(term->flash.fd, 0, &alarm, NULL) < 0) LOG_ERRNO("failed to arm flash timer"); else { term->flash.active = true; } } void term_bell(struct terminal *term) { if (!term->bell_action_enabled) return; if (term->conf->bell.urgent && !term->kbd_focus) { if (!wayl_win_set_urgent(term->window)) { /* * Urgency (xdg-activation) is relatively new in * Wayland. Fallback to our old, "faked", urgency - * rendering our window margins in red */ term->render.urgency = true; term_damage_margins(term); } } if (term->conf->bell.system_bell) wayl_win_ring_bell(term->window); if (term->conf->bell.notify) { notify_notify(term, &(struct notification){ .title = xstrdup("Bell"), .body = xstrdup("Bell in terminal"), .expire_time = -1, .focus = true, }); } if (term->conf->bell.flash) term_flash(term, 100); if ((term->conf->bell.command.argv.args != NULL) && (!term->kbd_focus || term->conf->bell.command_focused)) { int devnull = open("/dev/null", O_RDONLY); spawn(term->reaper, NULL, term->conf->bell.command.argv.args, devnull, -1, -1, NULL, NULL, NULL); if (devnull >= 0) close(devnull); } } bool term_spawn_new(const struct terminal *term) { return spawn( term->reaper, term->cwd, (char *const []){term->foot_exe, NULL}, -1, -1, -1, NULL, NULL, NULL) >= 0; } void term_enable_app_sync_updates(struct terminal *term) { term->render.app_sync_updates.enabled = true; if (timerfd_settime( term->render.app_sync_updates.timer_fd, 0, &(struct itimerspec){.it_value = {.tv_sec = 1}}, NULL) < 0) { LOG_ERR("failed to arm timer for application synchronized updates"); } /* Disable pending refresh *iff* the grid is the *only* thing * scheduled to be re-rendered */ if (!term->render.refresh.csd && !term->render.refresh.search && !term->render.pending.csd && !term->render.pending.search) { term->render.refresh.grid = false; term->render.pending.grid = false; } /* Disarm delayed rendering timers */ timerfd_settime( term->delayed_render_timer.lower_fd, 0, &(struct itimerspec){{0}}, NULL); timerfd_settime( term->delayed_render_timer.upper_fd, 0, &(struct itimerspec){{0}}, NULL); term->delayed_render_timer.is_armed = false; } void term_disable_app_sync_updates(struct terminal *term) { if (!term->render.app_sync_updates.enabled) return; term->render.app_sync_updates.enabled = false; render_refresh(term); /* Reset timers */ timerfd_settime( term->render.app_sync_updates.timer_fd, 0, &(struct itimerspec){{0}}, NULL); } static inline void print_linewrap(struct terminal *term) { if (likely(!term->grid->cursor.lcf)) { /* Not and end of line */ return; } if (unlikely(!term->auto_margin)) { /* Auto-wrap disabled */ return; } term->grid->cur_row->linebreak = false; term->grid->cursor.lcf = false; const int row = term->grid->cursor.point.row; if (row == term->scroll_region.end - 1) term_scroll(term, 1); else { const int new_row = min(row + 1, term->rows - 1); term->grid->cursor.point.row = new_row; term->grid->cur_row = grid_row(term->grid, new_row); } term->grid->cursor.point.col = 0; } static inline void print_insert(struct terminal *term, int width) { if (likely(!term->insert_mode)) return; xassert(width > 0); struct row *row = term->grid->cur_row; const size_t move_count = max(0, term->cols - term->grid->cursor.point.col - width); memmove( &row->cells[term->grid->cursor.point.col + width], &row->cells[term->grid->cursor.point.col], move_count * sizeof(struct cell)); /* Mark moved cells as dirty */ for (size_t i = term->grid->cursor.point.col + width; i < term->cols; i++) row->cells[i].attrs.clean = 0; } static void print_spacer(struct terminal *term, int col, int remaining) { struct grid *grid = term->grid; struct row *row = grid->cur_row; struct cell *cell = &row->cells[col]; cell->wc = CELL_SPACER + remaining; cell->attrs = (struct attributes){0}; } /* * Puts a character on the grid. Coordinates are in screen coordinates * (i.e. ‘cursor’ coordinates). * * Does NOT: * - update the cursor * - linewrap * - erase sixels * * Limitations: * - double width characters not supported */ void term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, bool use_sgr_attrs) { struct row *row = grid_row(term->grid, r); row->dirty = true; xassert(c + count <= term->cols); struct attributes attrs = use_sgr_attrs ? term->vt.attrs : (struct attributes){0}; const struct cell *last = &row->cells[c + count]; for (struct cell *cell = &row->cells[c]; cell < last; cell++) { cell->wc = data; cell->attrs = attrs; /* TODO: why do we print the URI here, and then erase it below? */ if (unlikely(use_sgr_attrs && term->vt.osc8.uri != NULL)) { grid_row_uri_range_put(row, c, term->vt.osc8.uri, term->vt.osc8.id); switch (term->conf->url.osc8_underline) { case OSC8_UNDERLINE_ALWAYS: cell->attrs.url = true; break; case OSC8_UNDERLINE_URL_MODE: break; } } if (unlikely(use_sgr_attrs && (term->vt.underline.style > UNDERLINE_SINGLE || term->vt.underline.color_src != COLOR_DEFAULT))) { grid_row_underline_range_put(row, c, term->vt.underline); } } if (unlikely(row->extra != NULL)) { if (likely(term->vt.osc8.uri != NULL)) grid_row_uri_range_erase(row, c, c + count - 1); if (likely(term->vt.underline.style <= UNDERLINE_SINGLE && term->vt.underline.color_src == COLOR_DEFAULT)) { /* No extended/styled underlines active, so erase any such attributes at the target columns */ grid_row_underline_range_erase(row, c, c + count - 1); } } } void term_print(struct terminal *term, char32_t wc, int width, bool insert_mode_disable) { xassert(width > 0); struct grid *grid = term->grid; if (unlikely(term->charsets.set[term->charsets.selected] == CHARSET_GRAPHIC) && wc >= 0x60 && wc <= 0x7e) { /* 0x60 - 0x7e */ static const char32_t vt100_0[] = { U'◆', U'▒', U'␉', U'␌', U'␍', U'␊', U'°', U'±', /* ` - g */ U'␤', U'␋', U'┘', U'┐', U'┌', U'└', U'┼', U'⎺', /* h - o */ U'⎻', U'─', U'⎼', U'⎽', U'├', U'┤', U'┴', U'┬', /* p - w */ U'│', U'≤', U'≥', U'π', U'≠', U'£', U'·', /* x - ~ */ }; xassert(width == 1); wc = vt100_0[wc - 0x60]; } print_linewrap(term); if (!insert_mode_disable) print_insert(term, width); int col = grid->cursor.point.col; if (unlikely(width > 1) && likely(term->auto_margin) && col + width > term->cols) { /* Multi-column character that doesn't fit on current line - * pad with spacers */ for (size_t i = col; i < term->cols; i++) print_spacer(term, i, 0); /* And force a line-wrap */ grid->cursor.lcf = 1; print_linewrap(term); col = 0; } sixel_overwrite_at_cursor(term, width); /* *Must* get current cell *after* linewrap+insert */ struct row *row = grid->cur_row; row->dirty = true; row->linebreak = true; struct cell *cell = &row->cells[col]; cell->wc = term->vt.last_printed = wc; cell->attrs = term->vt.attrs; if (term->vt.osc8.uri != NULL) { grid_row_uri_range_put( row, col, term->vt.osc8.uri, term->vt.osc8.id); switch (term->conf->url.osc8_underline) { case OSC8_UNDERLINE_ALWAYS: cell->attrs.url = true; break; case OSC8_UNDERLINE_URL_MODE: break; } } else if (row->extra != NULL) grid_row_uri_range_erase(row, col, col + width - 1); if (unlikely(term->vt.underline.style > UNDERLINE_SINGLE || term->vt.underline.color_src != COLOR_DEFAULT)) { grid_row_underline_range_put(row, col, term->vt.underline); } else if (row->extra != NULL) grid_row_underline_range_erase(row, col, col + width - 1); /* Advance cursor the 'additional' columns while dirty:ing the cells */ for (int i = 1; i < width && (col + 1) < term->cols; i++) { col++; print_spacer(term, col, width - i); } xassert(col < term->cols); /* Advance cursor */ if (unlikely(++col >= term->cols)) { grid->cursor.lcf = true; col--; } else xassert(!grid->cursor.lcf); grid->cursor.point.col = col; } static void ascii_printer_generic(struct terminal *term, char32_t wc) { term_print(term, wc, 1, false); } static void ascii_printer_fast(struct terminal *term, char32_t wc) { struct grid *grid = term->grid; xassert(term->charsets.set[term->charsets.selected] == CHARSET_ASCII); xassert(!term->insert_mode); xassert(tll_length(grid->sixel_images) == 0); print_linewrap(term); /* *Must* get current cell *after* linewrap+insert */ int col = grid->cursor.point.col; const int uri_start = col; struct row *row = grid->cur_row; row->dirty = true; row->linebreak = true; struct cell *cell = &row->cells[col]; cell->wc = term->vt.last_printed = wc; cell->attrs = term->vt.attrs; /* Advance cursor */ if (unlikely(++col >= term->cols)) { xassert(col == term->cols); grid->cursor.lcf = true; col--; } else xassert(!grid->cursor.lcf); grid->cursor.point.col = col; if (unlikely(row->extra != NULL)) { grid_row_uri_range_erase(row, uri_start, uri_start); grid_row_underline_range_erase(row, uri_start, uri_start); } } static void ascii_printer_single_shift(struct terminal *term, char32_t wc) { ascii_printer_generic(term, wc); term->charsets.selected = term->charsets.saved; term->bits_affecting_ascii_printer.charset = term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); } void term_update_ascii_printer(struct terminal *term) { _Static_assert(sizeof(term->bits_affecting_ascii_printer) == sizeof(uint8_t), "bad size"); void (*new_printer)(struct terminal *term, char32_t wc) = unlikely(term->bits_affecting_ascii_printer.value != 0) ? &ascii_printer_generic : &ascii_printer_fast; #if defined(_DEBUG) && LOG_ENABLE_DBG if (term->ascii_printer != new_printer) { LOG_DBG("switching ASCII printer %s -> %s", term->ascii_printer == &ascii_printer_fast ? "fast" : "generic", new_printer == &ascii_printer_fast ? "fast" : "generic"); } #endif term->ascii_printer = new_printer; } void term_single_shift(struct terminal *term, enum charset_designator idx) { term->charsets.saved = term->charsets.selected; term->charsets.selected = idx; term->ascii_printer = &ascii_printer_single_shift; } #if defined(FOOT_GRAPHEME_CLUSTERING) static int emoji_vs_compare(const void *_key, const void *_entry) { const struct emoji_vs *key = _key; const struct emoji_vs *entry = _entry; uint32_t cp = key->start; if (cp < entry->start) return -1; else if (cp > entry->end) return 1; else return 0; } UNITTEST { /* Verify the emoji_vs list is sorted */ int64_t last_end = -1; for (size_t i = 0; i < sizeof(emoji_vs) / sizeof(emoji_vs[0]); i++) { const struct emoji_vs *vs = &emoji_vs[i]; xassert(vs->start <= vs->end); xassert(vs->start > last_end); xassert(vs->vs15 || vs->vs16); last_end = vs->end; } } #endif void term_process_and_print_non_ascii(struct terminal *term, char32_t wc) { int width = c32width(wc); bool insert_mode_disable = false; const bool grapheme_clustering = term->grapheme_shaping; #if !defined(FOOT_GRAPHEME_CLUSTERING) xassert(!grapheme_clustering); #endif if (term->grid->cursor.point.col > 0 && (grapheme_clustering || (!grapheme_clustering && width == 0 && wc >= 0x300))) { int col = term->grid->cursor.point.col; if (!term->grid->cursor.lcf) col--; /* Skip past spacers */ struct row *row = term->grid->cur_row; while (row->cells[col].wc >= CELL_SPACER && col > 0) col--; xassert(col >= 0 && col < term->cols); char32_t base = row->cells[col].wc; char32_t UNUSED last = base; /* Is base cell already a cluster? */ const struct composed *composed = (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) ? composed_lookup(term->composed, base - CELL_COMB_CHARS_LO) : NULL; uint32_t key; if (composed != NULL) { base = composed->chars[0]; last = composed->chars[composed->count - 1]; key = composed_key_from_key(composed->key, wc); } else key = composed_key_from_key(base, wc); #if defined(FOOT_GRAPHEME_CLUSTERING) if (grapheme_clustering) { /* Check if we're on a grapheme cluster break */ if (utf8proc_grapheme_break_stateful( last, wc, &term->vt.grapheme_state) && width > 0) { term_reset_grapheme_state(term); goto out; } } #endif int base_width = c32width(base); if (base_width > 0) { term->grid->cursor.point.col = col; term->grid->cursor.lcf = false; insert_mode_disable = true; if (composed == NULL) { bool base_from_primary; bool comb_from_primary; bool pre_from_primary; char32_t precomposed = term->fonts[0] != NULL ? fcft_precompose( term->fonts[0], base, wc, &base_from_primary, &comb_from_primary, &pre_from_primary) : (char32_t)-1; int precomposed_width = c32width(precomposed); /* * Only use the pre-composed character if: * * 1. we *have* a pre-composed character * 2. the width matches the base characters width * 3. it's in the primary font, OR one of the base or * combining characters are *not* from the primary * font */ if (precomposed != (char32_t)-1 && precomposed_width == base_width && (pre_from_primary || !base_from_primary || !comb_from_primary)) { wc = precomposed; width = precomposed_width; term_reset_grapheme_state(term); goto out; } } size_t wanted_count = composed != NULL ? composed->count + 1 : 2; if (wanted_count > 255) { xassert(composed != NULL); #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG LOG_WARN("combining character overflow:"); LOG_WARN(" base: 0x%04x", composed->chars[0]); for (size_t i = 1; i < composed->count; i++) LOG_WARN(" cc: 0x%04x", composed->chars[i]); LOG_ERR(" new: 0x%04x", wc); #endif /* This is going to break anyway... */ wanted_count--; } xassert(wanted_count <= 255); /* Check if we already have a match for the entire compose chain */ const struct composed *cc = composed_lookup_without_collision( term->composed, &key, composed != NULL ? composed->chars : &(char32_t){base}, composed != NULL ? composed->count : 1, wc, 0); if (cc != NULL) { /* We *do* have a match! */ wc = CELL_COMB_CHARS_LO + cc->key; width = cc->width; goto out; } else { /* No match - allocate a new chain below */ } if (unlikely(term->composed_count >= (CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO))) { /* We reached our maximum number of allowed composed * character chains. Fall through here and print the * current zero-width character to the current cell */ LOG_WARN("maximum number of composed characters reached"); term_reset_grapheme_state(term); goto out; } /* Allocate new chain */ struct composed *new_cc = xmalloc(sizeof(*new_cc)); new_cc->chars = xmalloc(wanted_count * sizeof(new_cc->chars[0])); new_cc->key = key; new_cc->count = wanted_count; new_cc->chars[0] = base; new_cc->chars[wanted_count - 1] = wc; new_cc->forced_width = composed != NULL ? composed->forced_width : 0; if (composed != NULL) { memcpy(&new_cc->chars[1], &composed->chars[1], (wanted_count - 2) * sizeof(new_cc->chars[0])); } const int grapheme_width = composed != NULL ? composed->width : base_width; switch (term->conf->tweak.grapheme_width_method) { case GRAPHEME_WIDTH_MAX: new_cc->width = max(grapheme_width, width); break; case GRAPHEME_WIDTH_DOUBLE: new_cc->width = min(grapheme_width + width, 2); #if defined(FOOT_GRAPHEME_CLUSTERING) /* Handle VS-15 and VS-16 variation selectors */ if (unlikely(grapheme_clustering && (wc == 0xfe0e || wc == 0xfe0f) && new_cc->count == 2)) { const struct emoji_vs *vs = bsearch( &(struct emoji_vs){.start = new_cc->chars[0]}, emoji_vs, sizeof(emoji_vs) / sizeof(emoji_vs[0]), sizeof(struct emoji_vs), &emoji_vs_compare); if (vs != NULL) { xassert(new_cc->chars[0] >= vs->start && new_cc->chars[0] <= vs->end); /* Force a grapheme width of 1 for VS-15, and 2 for VS-16 */ if (wc == 0xfe0e) { if (vs->vs15) new_cc->width = 1; } else if (wc == 0xfe0f) { if (vs->vs16) new_cc->width = 2; } } } #endif break; case GRAPHEME_WIDTH_WCSWIDTH: new_cc->width = grapheme_width + width; break; } term->composed_count++; composed_insert(&term->composed, new_cc); wc = CELL_COMB_CHARS_LO + new_cc->key; width = new_cc->forced_width > 0 ? new_cc->forced_width : new_cc->width; xassert(wc >= CELL_COMB_CHARS_LO); xassert(wc <= CELL_COMB_CHARS_HI); goto out; } } else term_reset_grapheme_state(term); out: if (width > 0) term_print(term, wc, width, insert_mode_disable); } enum term_surface term_surface_kind(const struct terminal *term, const struct wl_surface *surface) { if (likely(surface == term->window->surface.surf)) return TERM_SURF_GRID; else if (surface == term->window->csd.surface[CSD_SURF_TITLE].surface.surf) return TERM_SURF_TITLE; else if (surface == term->window->csd.surface[CSD_SURF_LEFT].surface.surf) return TERM_SURF_BORDER_LEFT; else if (surface == term->window->csd.surface[CSD_SURF_RIGHT].surface.surf) return TERM_SURF_BORDER_RIGHT; else if (surface == term->window->csd.surface[CSD_SURF_TOP].surface.surf) return TERM_SURF_BORDER_TOP; else if (surface == term->window->csd.surface[CSD_SURF_BOTTOM].surface.surf) return TERM_SURF_BORDER_BOTTOM; else if (surface == term->window->csd.surface[CSD_SURF_MINIMIZE].surface.surf) return TERM_SURF_BUTTON_MINIMIZE; else if (surface == term->window->csd.surface[CSD_SURF_MAXIMIZE].surface.surf) return TERM_SURF_BUTTON_MAXIMIZE; else if (surface == term->window->csd.surface[CSD_SURF_CLOSE].surface.surf) return TERM_SURF_BUTTON_CLOSE; else return TERM_SURF_NONE; } static bool rows_to_text(const struct terminal *term, int start, int end, int col_start, int col_end, char **text, size_t *len) { struct extraction_context *ctx = extract_begin(SELECTION_NONE, true); if (ctx == NULL) return false; const int grid_rows = term->grid->num_rows; int r = start; while (true) { const struct row *row = term->grid->rows[r]; xassert(row != NULL); const int c_end = r == end ? col_end : term->cols; for (int c = col_start; c < c_end; c++) { if (!extract_one(term, row, &row->cells[c], c, ctx)) goto out; } if (r == end) break; r++; r &= grid_rows - 1; col_start = 0; } out: return extract_finish(ctx, text, len); } bool term_scrollback_to_text(const struct terminal *term, char **text, size_t *len) { const int grid_rows = term->grid->num_rows; int start = (term->grid->offset + term->rows) & (grid_rows - 1); int end = (term->grid->offset + term->rows - 1) & (grid_rows - 1); xassert(start >= 0); xassert(start < grid_rows); xassert(end >= 0); xassert(end < grid_rows); /* If scrollback isn't full yet, this may be NULL, so scan forward * until we find the first non-NULL row */ while (term->grid->rows[start] == NULL) { start++; start &= grid_rows - 1; } while (term->grid->rows[end] == NULL) { end--; if (end < 0) end += term->grid->num_rows; } return rows_to_text(term, start, end, 0, term->cols, text, len); } bool term_view_to_text(const struct terminal *term, char **text, size_t *len) { int start = grid_row_absolute_in_view(term->grid, 0); int end = grid_row_absolute_in_view(term->grid, term->rows - 1); return rows_to_text(term, start, end, 0, term->cols, text, len); } bool term_command_output_to_text(const struct terminal *term, char **text, size_t *len) { int start_row = -1; int end_row = -1; int start_col = -1; int end_col = -1; const struct grid *grid = term->grid; const int sb_end = grid_row_absolute(grid, term->rows - 1); const int sb_start = (sb_end + 1) & (grid->num_rows - 1); int r = sb_end; while (start_row < 0) { const struct row *row = grid->rows[r]; if (row == NULL) break; if (row->shell_integration.cmd_end >= 0) { end_row = r; end_col = row->shell_integration.cmd_end; } if (end_row >= 0 && row->shell_integration.cmd_start >= 0) { start_row = r; start_col = row->shell_integration.cmd_start; } if (r == sb_start) break; r = (r - 1 + grid->num_rows) & (grid->num_rows - 1); } if (start_row < 0) return false; bool ret = rows_to_text(term, start_row, end_row, start_col, end_col, text, len); if (!ret) return false; /* * If the FTCS_COMMAND_FINISHED marker was emitted at the *first* * column, then the *entire* previous line is part of the command * output. *Including* the newline, if any. * * Since rows_to_text() doesn't extract the column * FTCS_COMMAND_FINISHED was emitted at (that would be wrong - * FTCS_COMMAND_FINISHED is emitted *after* the command output, * not at its last character), the extraction logic will not see * the last newline (this is true for all non-line-wise selection * types), and the extracted text will *not* end with a newline. * * Here we try to compensate for that. Note that if 'end_col' is * not 0, then the command output only covers a partial row, and * thus we do *not* want to append a newline. */ if (end_col > 0) { /* Command output covers partial row - don't append newline */ return true; } int next_to_last_row = (end_row - 1 + grid->num_rows) & (grid->num_rows - 1); const struct row *row = grid->rows[next_to_last_row]; /* Add newline if last row has a hard linebreak */ if (row->linebreak) { char *new_text = xrealloc(*text, *len + 1 + 1); if (new_text == NULL) { /* Ignore failure - use text as is (without inserting newline) */ return true; } *text = new_text; (*len)++; (*text)[*len - 1] = '\n'; (*text)[*len] = '\0'; } return true; } bool term_ime_is_enabled(const struct terminal *term) { #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED return term->ime_enabled; #else return false; #endif } void term_ime_enable(struct terminal *term) { #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (term->ime_enabled) return; LOG_DBG("IME enabled"); term->ime_enabled = true; /* IME is per seat - enable on all seat currently focusing us */ tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) ime_enable(&it->item); } #endif } void term_ime_disable(struct terminal *term) { #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (!term->ime_enabled) return; LOG_DBG("IME disabled"); term->ime_enabled = false; /* IME is per seat - disable on all seat currently focusing us */ tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) ime_disable(&it->item); } #endif } bool term_ime_reset(struct terminal *term) { bool at_least_one_seat_was_reset = false; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED tll_foreach(term->wl->seats, it) { struct seat *seat = &it->item; if (seat->kbd_focus != term) continue; ime_reset_preedit(seat); at_least_one_seat_was_reset = true; } #endif return at_least_one_seat_was_reset; } void term_ime_set_cursor_rect(struct terminal *term, int x, int y, int width, int height) { #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) { it->item.ime.cursor_rect.pending.x = x; it->item.ime.cursor_rect.pending.y = y; it->item.ime.cursor_rect.pending.width = width; it->item.ime.cursor_rect.pending.height = height; } } #endif } void term_osc8_open(struct terminal *term, uint64_t id, const char *uri) { term_osc8_close(term); xassert(term->vt.osc8.uri == NULL); term->vt.osc8.id = id; term->vt.osc8.uri = xstrdup(uri); term->bits_affecting_ascii_printer.osc8 = true; term_update_ascii_printer(term); } void term_osc8_close(struct terminal *term) { free(term->vt.osc8.uri); term->vt.osc8.uri = NULL; term->vt.osc8.id = 0; term->bits_affecting_ascii_printer.osc8 = false; term_update_ascii_printer(term); } void term_set_user_mouse_cursor(struct terminal *term, const char *cursor) { free(term->mouse_user_cursor); term->mouse_user_cursor = cursor != NULL && strlen(cursor) > 0 ? xstrdup(cursor) : NULL; term_xcursor_update(term); } void term_enable_size_notifications(struct terminal *term) { /* Note: always send current size upon activation, regardless of previous state */ term->size_notifications = true; term_send_size_notification(term); } void term_disable_size_notifications(struct terminal *term) { if (!term->size_notifications) return; term->size_notifications = false; } void term_send_size_notification(struct terminal *term) { if (!term->size_notifications) return; const int height = term->height - term->margins.top - term->margins.bottom; const int width = term->width - term->margins.left - term->margins.right; char buf[128]; const size_t n = xsnprintf( buf, sizeof(buf), "\033[48;%d;%d;%d;%dt", term->rows, term->cols, height, width); term_to_slave(term, buf, n); } foot-1.21.0/terminal.h000066400000000000000000000656771476600145200145600ustar00rootroot00000000000000#pragma once #include #include #include #include #include #if defined(FOOT_GRAPHEME_CLUSTERING) #include #endif #include #include #include "composed.h" #include "config.h" #include "debug.h" #include "fdm.h" #include "key-binding.h" #include "macros.h" #include "notify.h" #include "reaper.h" #include "shm.h" #include "wayland.h" enum color_source { COLOR_DEFAULT, COLOR_BASE16, COLOR_BASE256, COLOR_RGB, }; /* * Note: we want the cells to be as small as possible. Larger cells * means fewer scrollback lines (or performance drops due to cache * misses) * * Note that the members are laid out optimized for x86 */ struct attributes { bool bold:1; bool dim:1; bool italic:1; bool underline:1; bool strikethrough:1; bool blink:1; bool conceal:1; bool reverse:1; uint32_t fg:24; bool clean:1; enum color_source fg_src:2; enum color_source bg_src:2; bool confined:1; bool selected:1; bool url:1; uint32_t bg:24; }; static_assert(sizeof(struct attributes) == 8, "VT attribute struct too large"); /* Last valid Unicode code point is 0x0010FFFFul */ #define CELL_COMB_CHARS_LO 0x00200000ul #define CELL_COMB_CHARS_HI (CELL_COMB_CHARS_LO + 0x3fffffff) #define CELL_SPACER (CELL_COMB_CHARS_HI + 1) struct cell { char32_t wc; struct attributes attrs; }; static_assert(sizeof(struct cell) == 12, "bad size"); struct scroll_region { int start; int end; }; struct coord { int col; int row; }; struct range { struct coord start; struct coord end; }; struct cursor { struct coord point; bool lcf; /* Last Column Flag; https://github.com/mattiase/wraptest#basic-vt-line-wrapping-rules */ }; enum damage_type {DAMAGE_SCROLL, DAMAGE_SCROLL_REVERSE, DAMAGE_SCROLL_IN_VIEW, DAMAGE_SCROLL_REVERSE_IN_VIEW}; struct damage { enum damage_type type; struct scroll_region region; uint16_t lines; }; struct uri_range_data { uint64_t id; char *uri; }; 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 row_ranges uri_ranges; struct row_ranges underline_ranges; }; struct row { struct cell *cells; struct row_data *extra; bool dirty; bool linebreak; struct { bool prompt_marker; int cmd_start; /* Column, -1 if unset */ int cmd_end; /* Column, -1 if unset */ } shell_integration; }; struct sixel { /* * These three members reflect the "current", maybe scaled version * of the image. * * The values will either be NULL/-1/-1, or match either the * values in "original", or "scaled". * * They are typically reset when we need to invalidate the cached * version (e.g. when the cell dimensions change). */ pixman_image_t *pix; int width; int height; int rows; int cols; struct coord pos; bool opaque; /* * We store the cell dimensions of the time the sixel was emitted. * * If the font size is changed, we rescale the image accordingly, * to ensure it stays within its cell boundaries. 'scaled' is a * cached, rescaled version of 'data' + 'pix'. */ int cell_width; int cell_height; struct { void *data; pixman_image_t *pix; int width; int height; } original; struct { void *data; pixman_image_t *pix; int width; int height; } scaled; }; enum kitty_kbd_flags { KITTY_KBD_DISAMBIGUATE = 0x01, KITTY_KBD_REPORT_EVENT = 0x02, KITTY_KBD_REPORT_ALTERNATE = 0x04, KITTY_KBD_REPORT_ALL = 0x08, KITTY_KBD_REPORT_ASSOCIATED = 0x10, KITTY_KBD_SUPPORTED = (KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_EVENT | KITTY_KBD_REPORT_ALTERNATE | KITTY_KBD_REPORT_ALL | KITTY_KBD_REPORT_ASSOCIATED), }; struct grid { int num_rows; int num_cols; int offset; int view; /* * Note: the cursor (not the *saved* cursor) could most likely be * global state in the term struct. * * However, we have grid specific functions that does not have * access to the owning term struct, but does need access to the * cursor. */ struct cursor cursor; struct cursor saved_cursor; struct row **rows; struct row *cur_row; tll(struct damage) scroll_damage; tll(struct sixel) sixel_images; struct { enum kitty_kbd_flags flags[8]; uint8_t idx; } kitty_kbd; }; struct vt_subparams { uint8_t idx; unsigned *cur; unsigned value[16]; unsigned dummy; }; struct vt_param { unsigned value; struct vt_subparams sub; }; struct vt { int state; /* enum state */ char32_t last_printed; #if defined(FOOT_GRAPHEME_CLUSTERING) utf8proc_int32_t grapheme_state; #endif char32_t utf8; struct { uint8_t idx; struct vt_param *cur; struct vt_param v[16]; struct vt_param dummy; } params; uint32_t private; /* LSB=priv0, MSB=priv3 */ struct attributes attrs; struct attributes saved_attrs; struct { uint8_t *data; size_t size; size_t idx; bool bel; /* true if OSC string was terminated by BEL */ } osc; /* Start coordinate for current OSC-8 URI */ struct { uint64_t id; char *uri; } osc8; struct underline_range_data underline; struct { uint8_t *data; size_t size; size_t idx; void (*put_handler)(struct terminal *term, uint8_t c); void (*unhook_handler)(struct terminal *term); } dcs; }; enum cursor_origin { ORIGIN_ABSOLUTE, ORIGIN_RELATIVE }; enum cursor_keys { CURSOR_KEYS_DONTCARE, CURSOR_KEYS_NORMAL, CURSOR_KEYS_APPLICATION }; enum keypad_keys { KEYPAD_DONTCARE, KEYPAD_NUMERICAL, KEYPAD_APPLICATION }; enum charset { CHARSET_ASCII, CHARSET_GRAPHIC }; enum charset_designator { G0, G1, G2, G3 }; struct charsets { enum charset_designator selected; enum charset_designator saved; enum charset set[4]; /* G0-G3 */ }; /* *What* to report */ enum mouse_tracking { MOUSE_NONE, MOUSE_X10, /* ?9h */ MOUSE_CLICK, /* ?1000h - report mouse clicks */ MOUSE_DRAG, /* ?1002h - report clicks and drag motions */ MOUSE_MOTION, /* ?1003h - report clicks and motion */ }; /* *How* to report */ enum mouse_reporting { MOUSE_NORMAL, MOUSE_UTF8, /* ?1005h */ MOUSE_SGR, /* ?1006h */ MOUSE_URXVT, /* ?1015h */ MOUSE_SGR_PIXELS, /* ?1016h */ }; enum selection_kind { SELECTION_NONE, SELECTION_CHAR_WISE, SELECTION_WORD_WISE, SELECTION_QUOTE_WISE, SELECTION_LINE_WISE, SELECTION_BLOCK }; enum selection_direction {SELECTION_UNDIR, SELECTION_LEFT, SELECTION_RIGHT}; enum selection_scroll_direction {SELECTION_SCROLL_NOT, SELECTION_SCROLL_UP, SELECTION_SCROLL_DOWN}; enum search_direction { SEARCH_BACKWARD_SAME_POSITION, SEARCH_BACKWARD, SEARCH_FORWARD }; struct ptmx_buffer { void *data; size_t len; size_t idx; }; enum term_surface { TERM_SURF_NONE, TERM_SURF_GRID, TERM_SURF_TITLE, TERM_SURF_BORDER_LEFT, TERM_SURF_BORDER_RIGHT, TERM_SURF_BORDER_TOP, TERM_SURF_BORDER_BOTTOM, TERM_SURF_BUTTON_MINIMIZE, TERM_SURF_BUTTON_MAXIMIZE, TERM_SURF_BUTTON_CLOSE, }; enum overlay_style { OVERLAY_NONE, OVERLAY_SEARCH, OVERLAY_FLASH, OVERLAY_UNICODE_MODE, }; typedef tll(struct ptmx_buffer) ptmx_buffer_list_t; enum url_action { URL_ACTION_COPY, URL_ACTION_LAUNCH, URL_ACTION_PERSISTENT }; struct url { uint64_t id; char *url; char32_t *key; struct range range; enum url_action action; bool url_mode_dont_change_url_attr; /* Entering/exiting URL mode doesn't touch the cells' attr.url */ bool osc8; bool duplicate; }; typedef tll(struct url) url_list_t; struct colors { uint32_t fg; uint32_t bg; uint32_t table[256]; uint16_t alpha; uint32_t cursor_fg; /* Text color */ uint32_t cursor_bg; /* cursor color */ uint32_t selection_fg; uint32_t selection_bg; bool use_custom_selection; }; 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; struct vt vt; struct grid *grid; struct grid normal; struct grid alt; int cols; /* number of columns */ int rows; /* number of rows */ struct scroll_region scroll_region; struct charsets charsets; struct charsets saved_charsets; /* For save/restore cursor + attributes */ bool auto_margin; bool insert_mode; bool reverse; bool hide_cursor; bool reverse_wrap; bool bracketed_paste; bool focus_events; bool alt_scrolling; bool modify_other_keys_2; /* True when modifyOtherKeys=2 (i.e. "CSI >4;2m") */ enum cursor_origin origin; enum cursor_keys cursor_keys_mode; enum keypad_keys keypad_keys_mode; enum mouse_tracking mouse_tracking; enum mouse_reporting mouse_reporting; char *mouse_user_cursor; /* For OSC-22 */ tll(int) tab_stops; size_t composed_count; struct composed *composed; /* Temporary: for FDM */ struct { bool is_armed; int lower_fd; int upper_fd; } delayed_render_timer; struct fcft_font *fonts[4]; struct config_font *font_sizes[4]; struct pt_or_px font_line_height; float font_dpi; float font_dpi_before_unmap; bool font_is_sized_by_dpi; int16_t font_x_ofs; int16_t font_y_ofs; int16_t font_baseline; enum fcft_subpixel font_subpixel; struct { struct fcft_glyph **box_drawing; struct fcft_glyph **braille; struct fcft_glyph **octants; struct fcft_glyph **legacy; #define GLYPH_BOX_DRAWING_FIRST 0x2500 #define GLYPH_BOX_DRAWING_LAST 0x259F #define GLYPH_BOX_DRAWING_COUNT \ (GLYPH_BOX_DRAWING_LAST - GLYPH_BOX_DRAWING_FIRST + 1) #define GLYPH_BRAILLE_FIRST 0x2800 #define GLYPH_BRAILLE_LAST 0x28FF #define GLYPH_BRAILLE_COUNT \ (GLYPH_BRAILLE_LAST - GLYPH_BRAILLE_FIRST + 1) #define GLYPH_OCTANTS_FIRST 0x1CD00 #define GLYPH_OCTANTS_LAST 0x1CDE5 #define GLYPH_OCTANTS_COUNT \ (GLYPH_OCTANTS_LAST - GLYPH_OCTANTS_FIRST + 1) #define GLYPH_LEGACY_FIRST 0x1FB00 #define GLYPH_LEGACY_LAST 0x1FB9B #define GLYPH_LEGACY_COUNT \ (GLYPH_LEGACY_LAST - GLYPH_LEGACY_FIRST + 1) } custom_glyphs; bool is_sending_paste_data; ptmx_buffer_list_t ptmx_buffers; ptmx_buffer_list_t ptmx_paste_buffers; struct { bool esc_prefix; bool eight_bit; } meta; bool num_lock_modifier; bool bell_action_enabled; /* Saved DECSET modes - we save the SET state */ struct { bool origin:1; bool application_cursor_keys:1; bool application_keypad_keys:1; bool reverse:1; bool show_cursor:1; bool reverse_wrap:1; bool auto_margin:1; bool cursor_blink:1; bool bracketed_paste:1; bool focus_events:1; bool alt_scrolling:1; //bool mouse_x10:1; bool mouse_click:1; bool mouse_drag:1; bool mouse_motion:1; //bool mouse_utf8:1; bool mouse_sgr:1; bool mouse_urxvt:1; bool mouse_sgr_pixels:1; bool meta_eight_bit:1; bool meta_esc_prefix:1; bool num_lock_modifier:1; bool bell_action_enabled:1; bool alt_screen:1; bool ime:1; bool app_sync_updates:1; bool grapheme_shaping:1; bool size_notifications:1; bool sixel_display_mode:1; bool sixel_private_palette:1; bool sixel_cursor_right_of_graphics:1; } xtsave; bool window_title_has_been_set; char *window_title; tll(char *) window_title_stack; //char *window_icon; /* No escape sequence available to set the icon */ //tll(char *)window_icon_stack; char *app_id; struct { bool active; int fd; } flash; struct { enum { BLINK_ON, BLINK_OFF } state; int fd; } blink; float scale; float scale_before_unmap; /* Last scaling factor used */ int width; /* pixels */ int height; /* pixels */ int stashed_width; int stashed_height; struct { int left; int right; int top; int bottom; } margins; int cell_width; /* pixels per cell, x-wise */ int cell_height; /* pixels per cell, y-wise */ struct colors colors; struct { struct colors *stack; size_t idx; size_t size; } color_stack; enum cursor_style cursor_style; struct { bool decset; /* Blink enabled via '\E[?12h' */ bool deccsusr; /* Blink enabled via '\E[X q' */ int fd; enum { CURSOR_BLINK_ON, CURSOR_BLINK_OFF } state; } cursor_blink; struct { enum selection_kind kind; enum selection_direction direction; struct range coords; bool ongoing; bool spaces_only; /* SELECTION_SEMANTIC_WORD */ struct range pivot; struct { int fd; int col; enum selection_scroll_direction direction; } auto_scroll; } selection; bool is_searching; struct { char32_t *buf; size_t len; size_t sz; size_t cursor; int original_view; bool view_followed_offset; struct coord match; size_t match_len; struct { char32_t *buf; size_t len; } last; } search; struct wayland *wl; struct wl_window *window; bool visual_focus; bool kbd_focus; enum term_surface active_surface; struct { struct { struct buffer_chain *grid; struct buffer_chain *search; struct buffer_chain *scrollback_indicator; struct buffer_chain *render_timer; struct buffer_chain *url; struct buffer_chain *csd; struct buffer_chain *overlay; } chains; /* Scheduled for rendering, as soon-as-possible */ struct { bool grid; bool csd; bool search; bool urls; } refresh; /* Scheduled for rendering, in the next frame callback */ struct { bool grid; bool csd; bool search; bool urls; } pending; bool margins; /* Someone explicitly requested a refresh of the margins */ bool urgency; /* Signal 'urgency' (paint borders red) */ struct { struct timespec last_update; int timer_fd; } title; struct { struct timespec last_update; int timer_fd; } icon; struct { struct timespec last_update; int timer_fd; } app_id; uint32_t scrollback_lines; /* Number of scrollback lines, from conf (TODO: move out from render struct?) */ struct { bool enabled; int timer_fd; } app_sync_updates; /* Render threads + synchronization primitives */ struct { uint16_t count; sem_t start; sem_t done; mtx_t lock; tll(int) queue; thrd_t *threads; struct buffer *buf; } workers; /* Last rendered cursor position */ struct { struct row *row; int col; bool hidden; } last_cursor; struct buffer *last_buf; /* Buffer we rendered to last time */ enum overlay_style last_overlay_style; struct buffer *last_overlay_buf; pixman_region32_t last_overlay_clip; size_t search_glyph_offset; struct timespec input_time; } render; struct { struct grid *grid; /* Original 'normal' grid, before resize started */ int old_screen_rows; /* term->rows before resize started */ int old_cols; /* term->cols before resize started */ int old_hide_cursor; /* term->hide_cursor before resize started */ int new_rows; /* New number of scrollback rows */ struct range selection_coords; } interactive_resizing; struct { enum { SIXEL_DECSIXEL, /* DECSIXEL body part ", $, -, ? ... ~ */ SIXEL_DECGRA, /* DECGRA Set Raster Attributes " Pan; Pad; Ph; Pv */ SIXEL_DECGRI, /* DECGRI Graphics Repeat Introducer ! Pn Ch */ SIXEL_DECGCI, /* DECGCI Graphics Color Introducer # Pc; Pu; Px; Py; Pz */ } state; struct coord pos; /* Current sixel coordinate */ int color_idx; /* Current palette index */ uint32_t *private_palette; /* Private palette, used when private mode 1070 is enabled */ uint32_t *shared_palette; /* Shared palette, used when private mode 1070 is disabled */ uint32_t *palette; /* Points to either private_palette or shared_palette */ uint32_t color; struct { uint32_t *data; /* Raw image data, in ARGB */ uint32_t *p; /* Pointer into data, for current position */ int width; /* Image width, in pixels */ int height; /* Image height, in pixels */ int alloc_height; unsigned int bottom_pixel; } image; /* * Pan is the vertical shape of a pixel * Pad is the horizontal shape of a pixel * * pan/pad is the sixel's aspect ratio */ int pan; int pad; bool scrolling:1; /* Private mode 80 */ bool use_private_palette:1; /* Private mode 1070 */ bool cursor_right_of_graphics:1; /* Private mode 8452 */ unsigned params[5]; /* Collected parameters, for RASTER, COLOR_SPEC */ unsigned param; /* Currently collecting parameter, for RASTER, COLOR_SPEC and REPEAT */ unsigned param_idx; /* Parameters seen */ unsigned repeat_count; bool transparent_bg; bool linear_blending; bool use_10bit; pixman_format_code_t pixman_fmt; /* Application configurable */ unsigned palette_size; /* Number of colors in palette */ unsigned max_width; /* Maximum image width, in pixels */ unsigned max_height; /* Maximum image height, in pixels */ } sixel; /* TODO: wrap in a struct */ url_list_t urls; char32_t url_keys[5]; bool urls_show_uri_on_jump_label; struct grid *url_grid_snapshot; bool ime_reenable_after_url_mode; const struct config_spawn_template *url_launch; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED bool ime_enabled; #endif struct { bool active; int count; char32_t character; } unicode_mode; struct { bool in_progress; bool client_has_terminated; int terminate_timeout_fd; int exit_status; int next_signal; void (*cb)(void *data, int exit_code); void *cb_data; } shutdown; /* State, to handle chunked notifications */ struct notification kitty_notification; /* Currently active notifications, from foot's perspective (their notification helper processes are still running) */ tll(struct notification) active_notifications; struct notification_icon notification_icons[32]; char *foot_exe; char *cwd; bool grapheme_shaping; bool size_notifications; }; struct config; struct terminal *term_init( const struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl, const char *foot_exe, const char *cwd, const char *token, const char *pty_path, int argc, char *const *argv, const char *const *envp, void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data); bool term_shutdown(struct terminal *term); int term_destroy(struct terminal *term); void term_update_ascii_printer(struct terminal *term); void term_single_shift(struct terminal *term, enum charset_designator idx); void term_reset(struct terminal *term, bool hard); bool term_to_slave(struct terminal *term, const void *data, size_t len); bool term_paste_data_to_slave( struct terminal *term, const void *data, size_t len); bool term_fractional_scaling(const struct terminal *term); bool term_preferred_buffer_scale(const struct terminal *term); bool term_update_scale(struct terminal *term); bool term_font_size_increase(struct terminal *term); bool term_font_size_decrease(struct terminal *term); bool term_font_size_reset(struct terminal *term); bool term_font_dpi_changed(struct terminal *term, float old_scale); void term_font_subpixel_changed(struct terminal *term); int term_font_baseline(const struct terminal *term); int term_pt_or_px_as_pixels( const struct terminal *term, const struct pt_or_px *pt_or_px); void term_window_configured(struct terminal *term); void term_damage_rows(struct terminal *term, int start, int end); void term_damage_rows_in_view(struct terminal *term, int start, int end); void term_damage_all(struct terminal *term); void term_damage_view(struct terminal *term); void term_damage_cursor(struct terminal *term); void term_damage_margins(struct terminal *term); void term_damage_color(struct terminal *term, enum color_source src, int idx); void term_reset_view(struct terminal *term); void term_damage_scroll( struct terminal *term, enum damage_type damage_type, struct scroll_region region, int lines); void term_erase( struct terminal *term, int start_row, int start_col, int end_row, int end_col); void term_erase_scrollback(struct terminal *term); int term_row_rel_to_abs(const struct terminal *term, int row); void term_cursor_home(struct terminal *term); void term_cursor_to(struct terminal *term, int row, int col); void term_cursor_col(struct terminal *term, int col); void term_cursor_left(struct terminal *term, int count); void term_cursor_right(struct terminal *term, int count); void term_cursor_up(struct terminal *term, int count); void term_cursor_down(struct terminal *term, int count); void term_cursor_blink_update(struct terminal *term); void term_process_and_print_non_ascii(struct terminal *term, char32_t wc); void term_print(struct terminal *term, char32_t wc, int width, bool insert_mode_disable); void term_fill(struct terminal *term, int row, int col, uint8_t c, size_t count, bool use_sgr_attrs); void term_scroll(struct terminal *term, int rows); void term_scroll_reverse(struct terminal *term, int rows); void term_scroll_partial( struct terminal *term, struct scroll_region region, int rows); void term_scroll_reverse_partial( struct terminal *term, struct scroll_region region, int rows); void term_carriage_return(struct terminal *term); void term_linefeed(struct terminal *term); void term_reverse_index(struct terminal *term); void term_arm_blink_timer(struct terminal *term); void term_save_cursor(struct terminal *term); void term_restore_cursor(struct terminal *term, const struct cursor *cursor); void term_visual_focus_in(struct terminal *term); void term_visual_focus_out(struct terminal *term); void term_kbd_focus_in(struct terminal *term); void term_kbd_focus_out(struct terminal *term); void term_mouse_down( struct terminal *term, int button, int row, int col, int row_pixels, int col_pixels, bool shift, bool alt, bool ctrl); void term_mouse_up( struct terminal *term, int button, int row, int col, int row_pixels, int col_pixels, bool shift, bool alt, bool ctrl); void term_mouse_motion( struct terminal *term, int button, int row, int col, int row_pixels, int col_pixels, bool shift, bool alt, bool ctrl); bool term_mouse_grabbed(const struct terminal *term, const struct seat *seat); void term_xcursor_update(struct terminal *term); void term_xcursor_update_for_seat(struct terminal *term, struct seat *seat); void term_set_user_mouse_cursor(struct terminal *term, const char *cursor); void term_set_window_title(struct terminal *term, const char *title); void term_set_app_id(struct terminal *term, const char *app_id); const char *term_icon(const struct terminal *term); void term_flash(struct terminal *term, unsigned duration_ms); void term_bell(struct terminal *term); bool term_spawn_new(const struct terminal *term); void term_enable_app_sync_updates(struct terminal *term); void term_disable_app_sync_updates(struct terminal *term); enum term_surface term_surface_kind( const struct terminal *term, const struct wl_surface *surface); bool term_scrollback_to_text( const struct terminal *term, char **text, size_t *len); bool term_view_to_text( const struct terminal *term, char **text, size_t *len); bool term_command_output_to_text( const struct terminal *term, char **text, size_t *len); bool term_ime_is_enabled(const struct terminal *term); void term_ime_enable(struct terminal *term); void term_ime_disable(struct terminal *term); bool term_ime_reset(struct terminal *term); void term_ime_set_cursor_rect( struct terminal *term, int x, int y, int width, int height); void term_urls_reset(struct terminal *term); void term_collect_urls(struct terminal *term); void term_osc8_open(struct terminal *term, uint64_t id, const char *uri); void term_osc8_close(struct terminal *term); bool term_ptmx_pause(struct terminal *term); bool term_ptmx_resume(struct terminal *term); void term_enable_size_notifications(struct terminal *term); void term_disable_size_notifications(struct terminal *term); void term_send_size_notification(struct terminal *term); static inline void term_reset_grapheme_state(struct terminal *term) { #if defined(FOOT_GRAPHEME_CLUSTERING) term->vt.grapheme_state = 0; #endif } foot-1.21.0/tests/000077500000000000000000000000001476600145200137115ustar00rootroot00000000000000foot-1.21.0/tests/meson.build000066400000000000000000000003311476600145200160500ustar00rootroot00000000000000config_test = executable( 'test-config', 'test-config.c', wl_proto_headers, link_with: [common, tokenize], dependencies: [pixman, xkb, fontconfig, wayland_client, fcft, tllist]) test('config', config_test) foot-1.21.0/tests/test-config.c000066400000000000000000001355531476600145200163130ustar00rootroot00000000000000#if !defined(_DEBUG) #define _DEBUG #endif #undef NDEBUG #include "../log.h" #include "../config.c" #define ALEN(v) (sizeof(v) / sizeof((v)[0])) /* * Stubs */ void user_notification_add_fmt(user_notifications_t *notifications, enum user_notification_kind kind, const char *fmt, ...) { } static void test_invalid_key(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key) { ctx->key = key; ctx->value = "value for invalid key"; if (parse_fun(ctx)) { BUG("[%s].%s: did not fail to parse as expected" "(key should be invalid)", ctx->section, ctx->key); } } static void test_string(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key, char *const *ptr) { ctx->key = key; static const struct { const char *option_string; const char *value; bool invalid; } input[] = { {"a string", "a string"}, }; for (size_t i = 0; i < ALEN(input); i++) { ctx->value = input[i].option_string; if (input[i].invalid) { if (parse_fun(ctx)) { BUG("[%s].%s=%s: did not fail to parse as expected", ctx->section, ctx->key, ctx->value); } } else { if (!parse_fun(ctx)) { BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); } if (!streq(*ptr, input[i].value)) { BUG("[%s].%s=%s: set value (%s) not the expected one (%s)", ctx->section, ctx->key, ctx->value, *ptr, input[i].value); } } } } static void test_c32string(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key, char32_t *const *ptr) { ctx->key = key; static const struct { const char *option_string; const char32_t *value; bool invalid; } input[] = { {"a string", U"a string"}, }; for (size_t i = 0; i < ALEN(input); i++) { ctx->value = input[i].option_string; if (input[i].invalid) { if (parse_fun(ctx)) { BUG("[%s].%s=%s: did not fail to parse as expected", ctx->section, ctx->key, ctx->value); } } else { if (!parse_fun(ctx)) { BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); } if (c32cmp(*ptr, input[i].value) != 0) { BUG("[%s].%s=%s: set value (%ls) not the expected one (%ls)", ctx->section, ctx->key, ctx->value, (const wchar_t *)*ptr, (const wchar_t *)input[i].value); } } } } static void test_boolean(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key, const bool *ptr) { ctx->key = key; static const struct { const char *option_string; bool value; bool invalid; } input[] = { {"1", true}, {"0", false}, {"on", true}, {"off", false}, {"true", true}, {"false", false}, {"unittest-invalid-boolean-value", false, true}, }; for (size_t i = 0; i < ALEN(input); i++) { ctx->value = input[i].option_string; if (input[i].invalid) { if (parse_fun(ctx)) { BUG("[%s].%s=%s: did not fail to parse as expected", ctx->section, ctx->key, ctx->value); } } else { if (!parse_fun(ctx)) { BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); } if (*ptr != input[i].value) { BUG("[%s].%s=%s: set value (%s) not the expected one (%s)", ctx->section, ctx->key, ctx->value, *ptr ? "true" : "false", input[i].value ? "true" : "false"); } } } } static void test_uint16(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key, const uint16_t *ptr) { ctx->key = key; static const struct { const char *option_string; uint16_t value; bool invalid; } input[] = { {"0", 0}, {"65535", 65535}, {"65536", 0, true}, {"abc", 0, true}, {"true", 0, true}, }; for (size_t i = 0; i < ALEN(input); i++) { ctx->value = input[i].option_string; if (input[i].invalid) { if (parse_fun(ctx)) { BUG("[%s].%s=%s: did not fail to parse as expected", ctx->section, ctx->key, ctx->value); } } else { if (!parse_fun(ctx)) { BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); } if (*ptr != input[i].value) { BUG("[%s].%s=%s: set value (%hu) not the expected one (%hu)", ctx->section, ctx->key, ctx->value, *ptr, input[i].value); } } } } static void test_uint32(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key, const uint32_t *ptr) { ctx->key = key; static const struct { const char *option_string; uint32_t value; bool invalid; } input[] = { {"0", 0}, {"65536", 65536}, {"4294967295", 4294967295}, {"4294967296", 0, true}, {"abc", 0, true}, {"true", 0, true}, }; for (size_t i = 0; i < ALEN(input); i++) { ctx->value = input[i].option_string; if (input[i].invalid) { if (parse_fun(ctx)) { BUG("[%s].%s=%s: did not fail to parse as expected", ctx->section, ctx->key, ctx->value); } } else { if (!parse_fun(ctx)) { BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); } if (*ptr != input[i].value) { BUG("[%s].%s=%s: set value (%u) not the expected one (%u)", ctx->section, ctx->key, ctx->value, *ptr, input[i].value); } } } } static void test_float(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key, const float *ptr) { ctx->key = key; static const struct { const char *option_string; float value; bool invalid; } input[] = { {"0", 0}, {"0.1", 0.1}, {"1e10", 1e10}, {"-10.7", -10.7}, {"abc", 0, true}, {"true", 0, true}, }; for (size_t i = 0; i < ALEN(input); i++) { ctx->value = input[i].option_string; if (input[i].invalid) { if (parse_fun(ctx)) { BUG("[%s].%s=%s: did not fail to parse as expected", ctx->section, ctx->key, ctx->value); } } else { if (!parse_fun(ctx)) { BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); } if (*ptr != input[i].value) { BUG("[%s].%s=%s: set value (%f) not the expected one (%f)", ctx->section, ctx->key, ctx->value, *ptr, input[i].value); } } } } static void test_pt_or_px(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key, const struct pt_or_px *ptr) { ctx->key = key; static const struct { const char *option_string; struct pt_or_px value; bool invalid; } input[] = { {"12", {.pt = 12}}, {"12px", {.px = 12}}, {"unittest-invalid-pt-or-px-value", {0}, true}, }; for (size_t i = 0; i < ALEN(input); i++) { ctx->value = input[i].option_string; if (input[i].invalid) { if (parse_fun(ctx)) { BUG("[%s].%s=%s: did not fail to parse as expected", ctx->section, ctx->key, ctx->value); } } else { if (!parse_fun(ctx)) { BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); } if (memcmp(ptr, &input[i].value, sizeof(*ptr)) != 0) { BUG("[%s].%s=%s: " "set value (pt=%f, px=%d) not the expected one (pt=%f, px=%d)", ctx->section, ctx->key, ctx->value, ptr->pt, ptr->px, input[i].value.pt, input[i].value.px); } } } } static void test_spawn_template(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key, const struct config_spawn_template *ptr) { static const char *const args[] = { "command", "arg1", "arg2", "arg3 has spaces"}; ctx->key = key; ctx->value = "command arg1 arg2 \"arg3 has spaces\""; if (!parse_fun(ctx)) BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); if (ptr->argv.args == NULL) BUG("[%s].%s=%s: argv is NULL", ctx->section, ctx->key, ctx->value); for (size_t i = 0; i < ALEN(args); i++) { if (ptr->argv.args[i] == NULL || !streq(ptr->argv.args[i], args[i])) { BUG("[%s].%s=%s: set value not the expected one: " "mismatch of arg #%zu: expected=\"%s\", got=\"%s\"", ctx->section, ctx->key, ctx->value, i, args[i], ptr->argv.args[i]); } } if (ptr->argv.args[ALEN(args)] != NULL) { BUG("[%s].%s=%s: set value not the expected one: " "expected NULL terminator at arg #%zu, got=\"%s\"", ctx->section, ctx->key, ctx->value, ALEN(args), ptr->argv.args[ALEN(args)]); } /* Trigger parse failure */ ctx->value = "command with \"unterminated quote"; if (parse_fun(ctx)) { BUG("[%s].%s=%s: did not fail to parse as expected", ctx->section, ctx->key, ctx->value); } } static void test_enum(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key, size_t count, const char *enum_strings[static count], int enum_values[static count], int *ptr) { ctx->key = key; for (size_t i = 0; i < count; i++) { ctx->value = enum_strings[i]; if (!parse_fun(ctx)) { BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); } if (*ptr != enum_values[i]) { BUG("[%s].%s=%s: set value not the expected one: expected %d, got %d", ctx->section, ctx->key, ctx->value, enum_values[i], *ptr); } } ctx->value = "invalid-enum-value"; if (parse_fun(ctx)) { BUG("[%s].%s=%s: did not fail to parse as expected", ctx->section, ctx->key, ctx->value); } } static void test_color(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key, bool alpha_allowed, uint32_t *ptr) { ctx->key = key; const struct { const char *option_string; uint32_t color; bool invalid; } input[] = { {"000000", 0}, {"999999", 0x999999}, {"ffffff", 0xffffff}, {"ffffffff", 0xffffffff, !alpha_allowed}, {"aabbccdd", 0xaabbccdd, !alpha_allowed}, {"00", 0, true}, {"0000", 0, true}, {"00000", 0, true}, {"000000000", 0, true}, {"unittest-invalid-color", 0, true}, }; for (size_t i = 0; i < ALEN(input); i++) { ctx->value = input[i].option_string; if (input[i].invalid) { if (parse_fun(ctx)) { BUG("[%s].%s=%s: did not fail to parse as expected", ctx->section, ctx->key, ctx->value); } } else { if (!parse_fun(ctx)) { BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); } } } } static void test_two_colors(struct context *ctx, bool (*parse_fun)(struct context *ctx), const char *key, bool alpha_allowed, uint32_t *ptr1, uint32_t *ptr2) { ctx->key = key; const struct { const char *option_string; uint32_t color1; uint32_t color2; bool invalid; } input[] = { {"000000 000000", 0, 0}, /* No alpha */ {"999999 888888", 0x999999, 0x888888}, {"ffffff aaaaaa", 0xffffff, 0xaaaaaa}, /* Both colors have alpha component */ {"ffffffff 00000000", 0xffffffff, 0x00000000, !alpha_allowed}, {"aabbccdd, ee112233", 0xaabbccdd, 0xee112233, !alpha_allowed}, /* Only one color has alpha component */ {"ffffffff 112233", 0xffffffff, 0x112233, !alpha_allowed}, {"ffffff ff112233", 0x00ffffff, 0xff112233, !alpha_allowed}, {"unittest-invalid-color", 0, 0, true}, }; for (size_t i = 0; i < ALEN(input); i++) { ctx->value = input[i].option_string; if (input[i].invalid) { if (parse_fun(ctx)) { BUG("[%s].%s=%s: did not fail to parse as expected", ctx->section, ctx->key, ctx->value); } } else { if (!parse_fun(ctx)) { BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); } } } } static void test_section_main(void) { struct config conf = {0}; struct context ctx = {.conf = &conf, .section = "main", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_main, "invalid-key"); test_string(&ctx, &parse_section_main, "shell", &conf.shell); test_string(&ctx, &parse_section_main, "term", &conf.term); test_string(&ctx, &parse_section_main, "app-id", &conf.app_id); test_string(&ctx, &parse_section_main, "utmp-helper", &conf.utmp_helper_path); test_c32string(&ctx, &parse_section_main, "word-delimiters", &conf.word_delimiters); test_boolean(&ctx, &parse_section_main, "login-shell", &conf.login_shell); test_boolean(&ctx, &parse_section_main, "box-drawings-uses-font-glyphs", &conf.box_drawings_uses_font_glyphs); test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title); test_boolean(&ctx, &parse_section_main, "dpi-aware", &conf.dpi_aware); test_pt_or_px(&ctx, &parse_section_main, "font-size-adjustment", &conf.font_size_adjustment.pt_or_px); /* TODO: test ‘N%’ values too */ test_pt_or_px(&ctx, &parse_section_main, "line-height", &conf.line_height); test_pt_or_px(&ctx, &parse_section_main, "letter-spacing", &conf.letter_spacing); test_pt_or_px(&ctx, &parse_section_main, "horizontal-letter-offset", &conf.horizontal_letter_offset); test_pt_or_px(&ctx, &parse_section_main, "vertical-letter-offset", &conf.vertical_letter_offset); test_pt_or_px(&ctx, &parse_section_main, "underline-thickness", &conf.underline_thickness); test_pt_or_px(&ctx, &parse_section_main, "strikeout-thickness", &conf.strikeout_thickness); test_uint16(&ctx, &parse_section_main, "resize-delay-ms", &conf.resize_delay_ms); test_uint16(&ctx, &parse_section_main, "workers", &conf.render_worker_count); test_enum(&ctx, &parse_section_main, "selection-target", 4, (const char *[]){"none", "primary", "clipboard", "both"}, (int []){SELECTION_TARGET_NONE, SELECTION_TARGET_PRIMARY, SELECTION_TARGET_CLIPBOARD, SELECTION_TARGET_BOTH}, (int *)&conf.selection_target); test_enum( &ctx, &parse_section_main, "initial-window-mode", 3, (const char *[]){"windowed", "maximized", "fullscreen"}, (int []){STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN}, (int *)&conf.startup_mode); /* TODO: font (custom) */ /* TODO: include (custom) */ /* TODO: bold-text-in-bright (enum/boolean) */ /* TODO: pad (geometry + optional string)*/ /* TODO: initial-window-size-pixels (geometry) */ /* TODO: initial-window-size-chars (geometry) */ config_free(&conf); } static void test_section_security(void) { struct config conf = {0}; struct context ctx = {.conf = &conf, .section = "security", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_security, "invalid-key"); test_enum( &ctx, &parse_section_security, "osc52", 4, (const char*[]){"disabled", "copy-enabled", "paste-enabled", "enabled"}, (int []){OSC52_DISABLED, OSC52_COPY_ENABLED, OSC52_PASTE_ENABLED, OSC52_ENABLED}, (int *)&conf.security.osc52); config_free(&conf); } static void test_section_bell(void) { struct config conf = {0}; struct context ctx = {.conf = &conf, .section = "bell", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_bell, "invalid-key"); test_boolean(&ctx, &parse_section_bell, "urgent", &conf.bell.urgent); test_boolean(&ctx, &parse_section_bell, "notify", &conf.bell.notify); test_boolean(&ctx, &parse_section_bell, "system", &conf.bell.system_bell); test_boolean(&ctx, &parse_section_bell, "command-focused", &conf.bell.command_focused); test_spawn_template(&ctx, &parse_section_bell, "command", &conf.bell.command); config_free(&conf); } static void test_section_desktop_notifications(void) { struct config conf = {0}; struct context ctx = {.conf = &conf, .section = "desktop-notifications", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_desktop_notifications, "invalid-key"); test_boolean(&ctx, &parse_section_desktop_notifications, "inhibit-when-focused", &conf.desktop_notifications.inhibit_when_focused); test_spawn_template(&ctx, &parse_section_desktop_notifications, "command", &conf.desktop_notifications.command); test_spawn_template(&ctx, &parse_section_desktop_notifications, "command-action-argument", &conf.desktop_notifications.command_action_arg); test_spawn_template(&ctx, &parse_section_desktop_notifications, "close", &conf.desktop_notifications.close); config_free(&conf); } static void test_section_scrollback(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "scrollback", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_scrollback, "invalid-key"); test_uint32(&ctx, &parse_section_scrollback, "lines", &conf.scrollback.lines); test_float(&ctx, parse_section_scrollback, "multiplier", &conf.scrollback.multiplier); test_enum( &ctx, &parse_section_scrollback, "indicator-position", 3, (const char *[]){"none", "fixed", "relative"}, (int []){SCROLLBACK_INDICATOR_POSITION_NONE, SCROLLBACK_INDICATOR_POSITION_FIXED, SCROLLBACK_INDICATOR_POSITION_RELATIVE}, (int *)&conf.scrollback.indicator.position); /* TODO: indicator-format (enum, sort-of) */ config_free(&conf); } static void test_section_url(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "url", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_url, "invalid-key"); test_spawn_template(&ctx, &parse_section_url, "launch", &conf.url.launch); test_enum(&ctx, &parse_section_url, "osc8-underline", 2, (const char *[]){"url-mode", "always"}, (int []){OSC8_UNDERLINE_URL_MODE, OSC8_UNDERLINE_ALWAYS}, (int *)&conf.url.osc8_underline); test_c32string(&ctx, &parse_section_url, "label-letters", &conf.url.label_letters); config_free(&conf); } static void test_section_cursor(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "cursor", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_cursor, "invalid-key"); test_enum( &ctx, &parse_section_cursor, "style", 3, (const char *[]){"block", "beam", "underline"}, (int []){CURSOR_BLOCK, CURSOR_BEAM, CURSOR_UNDERLINE}, (int *)&conf.cursor.style); test_enum( &ctx, &parse_section_cursor, "unfocused-style", 3, (const char *[]){"unchanged", "hollow", "none"}, (int []){CURSOR_UNFOCUSED_UNCHANGED, CURSOR_UNFOCUSED_HOLLOW, CURSOR_UNFOCUSED_NONE}, (int *)&conf.cursor.unfocused_style); test_boolean(&ctx, &parse_section_cursor, "blink", &conf.cursor.blink.enabled); test_uint32(&ctx, &parse_section_cursor, "blink-rate", &conf.cursor.blink.rate_ms); test_pt_or_px(&ctx, &parse_section_cursor, "beam-thickness", &conf.cursor.beam_thickness); test_pt_or_px(&ctx, &parse_section_cursor, "underline-thickness", &conf.cursor.underline_thickness); /* TODO: color (two RRGGBB values) */ config_free(&conf); } static void test_section_mouse(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "mouse", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_mouse, "invalid-key"); test_boolean(&ctx, &parse_section_mouse, "hide-when-typing", &conf.mouse.hide_when_typing); test_boolean(&ctx, &parse_section_mouse, "alternate-scroll-mode", &conf.mouse.alternate_scroll_mode); config_free(&conf); } static void test_section_touch(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "touch", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_touch, "invalid-key"); test_uint32(&ctx, &parse_section_touch, "long-press-delay", &conf.touch.long_press_delay); config_free(&conf); } static void test_section_colors(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "colors", .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); 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_invalid_key(&ctx, &parse_section_colors, "256"); /* TODO: alpha (float in range 0-1, converted to uint16_t) */ config_free(&conf); } static void test_section_csd(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "csd", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_csd, "invalid-key"); test_enum( &ctx, &parse_section_csd, "preferred", 3, (const char *[]){"none", "client", "server"}, (int []){CONF_CSD_PREFER_NONE, CONF_CSD_PREFER_CLIENT, CONF_CSD_PREFER_SERVER}, (int *)&conf.csd.preferred); test_uint16(&ctx, &parse_section_csd, "size", &conf.csd.title_height); test_color(&ctx, &parse_section_csd, "color", true, &conf.csd.color.title); test_uint16(&ctx, &parse_section_csd, "border-width", &conf.csd.border_width_visible); test_color(&ctx, &parse_section_csd, "border-color", true, &conf.csd.color.border); test_uint16(&ctx, &parse_section_csd, "button-width", &conf.csd.button_width); test_color(&ctx, &parse_section_csd, "button-color", true, &conf.csd.color.buttons); test_color(&ctx, &parse_section_csd, "button-minimize-color", true, &conf.csd.color.minimize); test_color(&ctx, &parse_section_csd, "button-maximize-color", true, &conf.csd.color.maximize); test_color(&ctx, &parse_section_csd, "button-close-color", true, &conf.csd.color.quit); test_boolean(&ctx, &parse_section_csd, "hide-when-maximized", &conf.csd.hide_when_maximized); test_boolean(&ctx, &parse_section_csd, "double-click-to-maximize", &conf.csd.double_click_to_maximize); /* TODO: verify the ‘set’ bit is actually set for colors */ /* TODO: font */ config_free(&conf); } static bool have_modifier(const config_modifier_list_t *mods, const char *mod) { tll_foreach(*mods, it) { if (strcmp(it->item, mod) == 0) return true; } return false; } static void test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), int action, int max_action, const char *const *map, struct config_key_binding_list *bindings, enum key_binding_type type, bool need_argv, bool need_section_id) { xassert(map[action] != NULL); xassert(bindings->count == 0); const char *key = map[action]; /* “Randomize” which modifiers to enable */ const bool ctrl = action % 2; const bool alt = action % 3; const bool shift = action % 4; const bool super = action % 5; const bool argv = need_argv; const bool section_id = need_section_id; xassert(!(argv && section_id)); static const char *const args[] = { "command", "arg1", "arg2", "arg3 has spaces"}; /* Generate the modifier part of the ‘value’ */ char modifier_string[32]; sprintf(modifier_string, "%s%s%s%s", ctrl ? XKB_MOD_NAME_CTRL "+" : "", alt ? XKB_MOD_NAME_ALT "+" : "", shift ? XKB_MOD_NAME_SHIFT "+" : "", super ? XKB_MOD_NAME_LOGO "+" : ""); /* Use a unique symbol for this action (key bindings) */ const xkb_keysym_t sym = XKB_KEY_a + action; /* Mouse button (mouse bindings) */ const int button_idx = action % ALEN(button_map); const int button = button_map[button_idx].code; const int click_count = action % 3 + 1; /* Finally, generate the ‘value’ (e.g. “Control+shift+x”) */ char value[128] = {0}; ctx->key = key; ctx->value = value; /* First, try setting the empty string */ if (parse_fun(ctx)) { BUG("[%s].%s=: did not fail to parse as expected", ctx->section, ctx->key); } switch (type) { case KEY_BINDING: { char sym_name[16]; xkb_keysym_get_name(sym, sym_name, sizeof(sym_name)); snprintf(value, sizeof(value), "%s%s%s", argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : section_id ? "[foobar]" : "", modifier_string, sym_name); break; } case MOUSE_BINDING: { const char *const button_name = button_map[button_idx].name; int chars = snprintf( value, sizeof(value), "%s%s%s", argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : section_id ? "[foobar]" : "", modifier_string, button_name); xassert(click_count > 0); if (click_count > 1) snprintf(&value[chars], sizeof(value) - chars, "-%d", click_count); break; } } if (!parse_fun(ctx)) { BUG("[%s].%s=%s failed to parse", ctx->section, ctx->key, ctx->value); } const struct config_key_binding *binding = &bindings->arr[bindings->count - 1]; if (argv) { if (binding->aux.pipe.args == NULL) { BUG("[%s].%s=%s: pipe argv is NULL", ctx->section, ctx->key, ctx->value); } for (size_t i = 0; i < ALEN(args); i++) { if (binding->aux.pipe.args[i] == NULL || !streq(binding->aux.pipe.args[i], args[i])) { BUG("[%s].%s=%s: pipe argv not the expected one: " "mismatch of arg #%zu: expected=\"%s\", got=\"%s\"", ctx->section, ctx->key, ctx->value, i, args[i], binding->aux.pipe.args[i]); } } if (binding->aux.pipe.args[ALEN(args)] != NULL) { BUG("[%s].%s=%s: pipe argv not the expected one: " "expected NULL terminator at arg #%zu, got=\"%s\"", ctx->section, ctx->key, ctx->value, ALEN(args), binding->aux.pipe.args[ALEN(args)]); } } else if (section_id) { if (binding->aux.regex_name == NULL) { BUG("[%s].%s=%s: regex name is NULL", ctx->section, ctx->key, ctx->value); } if (!streq(binding->aux.regex_name, "foobar")) { BUG("[%s].%s=%s: regex name not the expected one: " "expected=\"%s\", got=\"%s\"", ctx->section, ctx->key, ctx->value, "foobar", binding->aux.regex_name); } } else { if (binding->aux.pipe.args != NULL) { BUG("[%s].%s=%s: pipe argv not NULL", ctx->section, ctx->key, ctx->value); } } if (binding->action != action) { BUG("[%s].%s=%s: action mismatch: %d != %d", ctx->section, ctx->key, ctx->value, binding->action, action); } bool have_ctrl = have_modifier(&binding->modifiers, XKB_MOD_NAME_CTRL); bool have_alt = have_modifier(&binding->modifiers, XKB_MOD_NAME_ALT); bool have_shift = have_modifier(&binding->modifiers, XKB_MOD_NAME_SHIFT); bool have_super = have_modifier(&binding->modifiers, XKB_MOD_NAME_LOGO); if (have_ctrl != ctrl || have_alt != alt || have_shift != shift || have_super != super) { BUG("[%s].%s=%s: modifier mismatch:\n" " have: ctrl=%d, alt=%d, shift=%d, super=%d\n" " expected: ctrl=%d, alt=%d, shift=%d, super=%d", ctx->section, ctx->key, ctx->value, have_ctrl, have_alt, have_shift, have_super, ctrl, alt, shift, super); } switch (type) { case KEY_BINDING: if (binding->k.sym != sym) { BUG("[%s].%s=%s: key symbol mismatch: %d != %d", ctx->section, ctx->key, ctx->value, binding->k.sym, sym); } break; case MOUSE_BINDING:; if (binding->m.button != button) { BUG("[%s].%s=%s: mouse button mismatch: %d != %d", ctx->section, ctx->key, ctx->value, binding->m.button, button); } if (binding->m.count != click_count) { BUG("[%s].%s=%s: mouse button click count mismatch: %d != %d", ctx->section, ctx->key, ctx->value, binding->m.count, click_count); } break; } free_key_binding_list(bindings); } enum collision_test_mode { FAIL_DIFFERENT_ACTION, FAIL_DIFFERENT_ARGV, FAIL_MOUSE_OVERRIDE, SUCCEED_SAME_ACTION_AND_ARGV, }; static void _test_binding_collisions(struct context *ctx, int max_action, const char *const *map, enum key_binding_type type, enum collision_test_mode test_mode) { struct config_key_binding *bindings_array = xcalloc(2, sizeof(bindings_array[0])); struct config_key_binding_list bindings = { .count = 2, .arr = bindings_array, }; /* First, verify we get a collision when trying to assign the same * key combo to multiple actions */ bindings.arr[0] = (struct config_key_binding){ .action = (test_mode == FAIL_DIFFERENT_ACTION ? max_action - 1 : max_action), .modifiers = tll_init(), .path = "unittest", }; tll_push_back(bindings.arr[0].modifiers, xstrdup(XKB_MOD_NAME_CTRL)); bindings.arr[1] = (struct config_key_binding){ .action = max_action, .modifiers = tll_init(), .path = "unittest", }; tll_push_back(bindings.arr[1].modifiers, xstrdup(XKB_MOD_NAME_CTRL)); switch (type) { case KEY_BINDING: bindings.arr[0].k.sym = XKB_KEY_a; bindings.arr[1].k.sym = XKB_KEY_a; break; case MOUSE_BINDING: bindings.arr[0].m.button = BTN_LEFT; bindings.arr[0].m.count = 1; bindings.arr[1].m.button = BTN_LEFT; bindings.arr[1].m.count = 1; break; } switch (test_mode) { case FAIL_DIFFERENT_ACTION: break; case FAIL_MOUSE_OVERRIDE: tll_free_and_free(ctx->conf->mouse.selection_override_modifiers, free); tll_push_back(ctx->conf->mouse.selection_override_modifiers, xstrdup(XKB_MOD_NAME_CTRL)); break; case FAIL_DIFFERENT_ARGV: case SUCCEED_SAME_ACTION_AND_ARGV: bindings.arr[0].aux.type = BINDING_AUX_PIPE; bindings.arr[0].aux.master_copy = true; bindings.arr[0].aux.pipe.args = xcalloc( 4, sizeof(bindings.arr[0].aux.pipe.args[0])); bindings.arr[0].aux.pipe.args[0] = xstrdup("/usr/bin/foobar"); bindings.arr[0].aux.pipe.args[1] = xstrdup("hello"); bindings.arr[0].aux.pipe.args[2] = xstrdup("world"); bindings.arr[1].aux.type = BINDING_AUX_PIPE; bindings.arr[1].aux.master_copy = true; bindings.arr[1].aux.pipe.args = xcalloc( 4, sizeof(bindings.arr[1].aux.pipe.args[0])); bindings.arr[1].aux.pipe.args[0] = xstrdup("/usr/bin/foobar"); bindings.arr[1].aux.pipe.args[1] = xstrdup("hello"); if (test_mode == SUCCEED_SAME_ACTION_AND_ARGV) bindings.arr[1].aux.pipe.args[2] = xstrdup("world"); break; } bool expected_result = test_mode == SUCCEED_SAME_ACTION_AND_ARGV ? true : false; if (resolve_key_binding_collisions( ctx->conf, ctx->section, map, &bindings, type) != expected_result) { BUG("[%s].%s vs. %s: %s", ctx->section, map[max_action - 1], map[max_action], (expected_result == true ? "invalid key combo collision detected" : "key combo collision not detected")); } if (expected_result == false) { if (bindings.count != 1) BUG("[%s]: colliding binding not removed", ctx->section); if (bindings.arr[0].action != (test_mode == FAIL_DIFFERENT_ACTION ? max_action - 1 : max_action)) { BUG("[%s]: wrong binding removed", ctx->section); } } free_key_binding_list(&bindings); } static void test_binding_collisions(struct context *ctx, int max_action, const char *const *map, enum key_binding_type type) { _test_binding_collisions(ctx, max_action, map, type, FAIL_DIFFERENT_ACTION); _test_binding_collisions(ctx, max_action, map, type, FAIL_DIFFERENT_ARGV); _test_binding_collisions(ctx, max_action, map, type, SUCCEED_SAME_ACTION_AND_ARGV); if (type == MOUSE_BINDING) { _test_binding_collisions( ctx, max_action, map, type, FAIL_MOUSE_OVERRIDE); } } static void test_section_key_bindings(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "key-bindings", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_key_bindings, "invalid-key"); for (int action = 0; action < BIND_ACTION_KEY_COUNT; action++) { if (binding_action_map[action] == NULL) continue; test_key_binding( &ctx, &parse_section_key_bindings, action, BIND_ACTION_KEY_COUNT - 1, binding_action_map, &conf.bindings.key, KEY_BINDING, action >= BIND_ACTION_PIPE_SCROLLBACK && action <= BIND_ACTION_PIPE_COMMAND_OUTPUT, action >= BIND_ACTION_REGEX_LAUNCH && action <= BIND_ACTION_REGEX_COPY); } config_free(&conf); } static void test_section_key_bindings_collisions(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "key-bindings", .path = "unittest"}; test_binding_collisions( &ctx, BIND_ACTION_KEY_COUNT - 1, binding_action_map, KEY_BINDING); config_free(&conf); } static void test_section_search_bindings(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "search-bindings", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_search_bindings, "invalid-key"); for (int action = 0; action < BIND_ACTION_SEARCH_COUNT; action++) { if (search_binding_action_map[action] == NULL) continue; test_key_binding( &ctx, &parse_section_search_bindings, action, BIND_ACTION_SEARCH_COUNT - 1, search_binding_action_map, &conf.bindings.search, KEY_BINDING, false, false); } config_free(&conf); } static void test_section_search_bindings_collisions(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "search-bindings", .path = "unittest"}; test_binding_collisions( &ctx, BIND_ACTION_SEARCH_COUNT - 1, search_binding_action_map, KEY_BINDING); config_free(&conf); } static void test_section_url_bindings(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "rul-bindings", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_url_bindings, "invalid-key"); for (int action = 0; action < BIND_ACTION_URL_COUNT; action++) { if (url_binding_action_map[action] == NULL) continue; test_key_binding( &ctx, &parse_section_url_bindings, action, BIND_ACTION_URL_COUNT - 1, url_binding_action_map, &conf.bindings.url, KEY_BINDING, false, false); } config_free(&conf); } static void test_section_url_bindings_collisions(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "url-bindings", .path = "unittest"}; test_binding_collisions( &ctx, BIND_ACTION_URL_COUNT - 1, url_binding_action_map, KEY_BINDING); config_free(&conf); } static void test_section_mouse_bindings(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "mouse-bindings", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_mouse_bindings, "invalid-key"); for (int action = 0; action < BIND_ACTION_COUNT; action++) { if (binding_action_map[action] == NULL) continue; test_key_binding( &ctx, &parse_section_mouse_bindings, action, BIND_ACTION_COUNT - 1, binding_action_map, &conf.bindings.mouse, MOUSE_BINDING, false, false); } config_free(&conf); } static void test_section_mouse_bindings_collisions(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "mouse-bindings", .path = "unittest"}; test_binding_collisions( &ctx, BIND_ACTION_COUNT - 1, binding_action_map, MOUSE_BINDING); config_free(&conf); } static void test_section_text_bindings(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "text-bindings", .path = "unittest"}; ctx.key = "abcd"; ctx.value = XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT "+x"; xassert(parse_section_text_bindings(&ctx)); ctx.key = "\\x07"; xassert(parse_section_text_bindings(&ctx)); ctx.key = "\\x1g"; xassert(!parse_section_text_bindings(&ctx)); ctx.key = "\\x1"; xassert(!parse_section_text_bindings(&ctx)); ctx.key = "\\x"; xassert(!parse_section_text_bindings(&ctx)); ctx.key = "\\"; xassert(!parse_section_text_bindings(&ctx)); ctx.key = "\\y"; xassert(!parse_section_text_bindings(&ctx)); #if 0 /* Invalid modifier and key names are detected later, when a * layout is applied */ ctx.key = "abcd"; ctx.value = "InvalidMod+y"; xassert(!parse_section_text_bindings(&ctx)); #endif config_free(&conf); } static void test_section_environment(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "environment", .path = "unittest"}; /* A single variable */ ctx.key = "FOO"; ctx.value = "bar"; xassert(parse_section_environment(&ctx)); xassert(tll_length(conf.env_vars) == 1); xassert(streq(tll_front(conf.env_vars).name, "FOO")); xassert(streq(tll_front(conf.env_vars).value, "bar")); /* Add a second variable */ ctx.key = "BAR"; ctx.value = "123"; xassert(parse_section_environment(&ctx)); xassert(tll_length(conf.env_vars) == 2); xassert(streq(tll_back(conf.env_vars).name, "BAR")); xassert(streq(tll_back(conf.env_vars).value, "123")); /* Replace the *value* of the first variable */ ctx.key = "FOO"; ctx.value = "456"; xassert(parse_section_environment(&ctx)); xassert(tll_length(conf.env_vars) == 2); xassert(streq(tll_front(conf.env_vars).name, "FOO")); xassert(streq(tll_front(conf.env_vars).value, "456")); xassert(streq(tll_back(conf.env_vars).name, "BAR")); xassert(streq(tll_back(conf.env_vars).value, "123")); config_free(&conf); } static void test_section_tweak(void) { struct config conf = {0}; struct context ctx = { .conf = &conf, .section = "tweak", .path = "unittest"}; test_invalid_key(&ctx, &parse_section_tweak, "invalid-key"); test_enum( &ctx, &parse_section_tweak, "scaling-filter", 5, (const char *[]){"none", "nearest", "bilinear", "cubic", "lanczos3"}, (int []){FCFT_SCALING_FILTER_NONE, FCFT_SCALING_FILTER_NEAREST, FCFT_SCALING_FILTER_BILINEAR, FCFT_SCALING_FILTER_CUBIC, FCFT_SCALING_FILTER_LANCZOS3}, (int *)&conf.tweak.fcft_filter); test_boolean(&ctx, &parse_section_tweak, "overflowing-glyphs", &conf.tweak.overflowing_glyphs); test_enum( &ctx, &parse_section_tweak, "render-timer", 4, (const char *[]){"none", "osd", "log", "both"}, (int []){RENDER_TIMER_NONE, RENDER_TIMER_OSD, RENDER_TIMER_LOG, RENDER_TIMER_BOTH}, (int *)&conf.tweak.render_timer); test_float(&ctx, &parse_section_tweak, "box-drawing-base-thickness", &conf.tweak.box_drawing_base_thickness); test_boolean(&ctx, &parse_section_tweak, "box-drawing-solid-shades", &conf.tweak.box_drawing_solid_shades); #if 0 /* Must be less than 16ms */ test_uint32(&ctx, &parse_section_tweak, "delayed-render-lower", &conf.tweak.delayed_render_lower_ns); test_uint32(&ctx, &parse_section_tweak, "delayed-render-upper", &conf.tweak.delayed_render_upper_ns); #endif test_boolean(&ctx, &parse_section_tweak, "damage-whole-window", &conf.tweak.damage_whole_window); #if defined(FOOT_GRAPHEME_CLUSTERING) test_boolean(&ctx, &parse_section_tweak, "grapheme-shaping", &conf.tweak.grapheme_shaping); #else /* TODO: the setting still exists, but is always forced to ‘false’. */ #endif test_enum( &ctx, &parse_section_tweak, "grapheme-width-method", 3, (const char *[]){"wcswidth", "double-width", "max"}, (int []){GRAPHEME_WIDTH_WCSWIDTH, GRAPHEME_WIDTH_DOUBLE, GRAPHEME_WIDTH_MAX}, (int *)&conf.tweak.grapheme_width_method); test_boolean(&ctx, &parse_section_tweak, "font-monospace-warn", &conf.tweak.font_monospace_warn); test_float(&ctx, &parse_section_tweak, "bold-text-in-bright-amount", &conf.bold_in_bright.amount); #if 0 /* Must be equal to, or less than INT32_MAX */ test_uint32(&ctx, &parse_section_tweak, "max-shm-pool-size-mb", &conf.tweak.max_shm_pool_size); #endif config_free(&conf); } int main(int argc, const char *const *argv) { FcInit(); log_init(LOG_COLORIZE_AUTO, false, 0, LOG_CLASS_ERROR); test_section_main(); test_section_security(); test_section_bell(); test_section_desktop_notifications(); test_section_scrollback(); test_section_url(); test_section_cursor(); test_section_mouse(); test_section_touch(); test_section_colors(); test_section_csd(); test_section_key_bindings(); test_section_key_bindings_collisions(); test_section_search_bindings(); test_section_search_bindings_collisions(); test_section_url_bindings(); test_section_url_bindings_collisions(); test_section_mouse_bindings(); test_section_mouse_bindings_collisions(); test_section_text_bindings(); test_section_environment(); test_section_tweak(); log_deinit(); FcFini(); return 0; } foot-1.21.0/themes/000077500000000000000000000000001476600145200140345ustar00rootroot00000000000000foot-1.21.0/themes/aeroroot000066400000000000000000000007101476600145200156070ustar00rootroot00000000000000# -*- conf -*- # Aero root theme [cursor] color=1a1a1a 9fd5f5 [colors] 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 foot-1.21.0/themes/apprentice000066400000000000000000000006661476600145200161210ustar00rootroot00000000000000# -*- conf -*- # https://github.com/romainl/Apprentice [cursor] color=262626 6c6c6c [colors] foreground=bcbcbc background=262626 regular0=1c1c1c regular1=af5f5f regular2=5f875f regular3=87875f regular4=5f87af regular5=5f5f87 regular6=5f8787 regular7=6c6c6c bright0=444444 bright1=ff8700 bright2=87af87 bright3=ffffaf bright4=87afd7 bright5=8787af bright6=5fafaf bright7=ffffff # selection-foreground=bcbcbc # selection-background=3a3e4efoot-1.21.0/themes/ayu-mirage000066400000000000000000000012531476600145200160200ustar00rootroot00000000000000# -*- conf -*- # theme: Ayu Mirage # description: a theme based on Ayu Mirage for Sublime Text (original: https://github.com/dempfi/ayu) [cursor] color = ffcc66 665a44 [colors] 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 foot-1.21.0/themes/catppuccin-frappe000066400000000000000000000007551476600145200173720ustar00rootroot00000000000000# _*_ conf _*_ # Catppuccin Frappe [colors] 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 selection-foreground=c6d0f5 selection-background=4f5369 search-box-no-match=232634 e78284 search-box-match=c6d0f5 414559 jump-labels=232634 ef9f76 urls=8caaee foot-1.21.0/themes/catppuccin-latte000066400000000000000000000007541476600145200172250ustar00rootroot00000000000000# _*_ conf _*_ # Catppuccin Latte [colors] 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 selection-foreground=4c4f69 selection-background=ccced7 search-box-no-match=dce0e8 d20f39 search-box-match=4c4f69 ccd0da jump-labels=dce0e8 fe640b urls=1e66f5 foot-1.21.0/themes/catppuccin-macchiato000066400000000000000000000007601476600145200200410ustar00rootroot00000000000000# _*_ conf _*_ # Catppuccin Macchiato [colors] 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 selection-foreground=cad3f5 selection-background=454a5f search-box-no-match=181926 ed8796 search-box-match=cad3f5 363a4f jump-labels=181926 f5a97f urls=8aadf4 foot-1.21.0/themes/catppuccin-mocha000066400000000000000000000007541476600145200172030ustar00rootroot00000000000000# _*_ conf _*_ # Catppuccin Mocha [colors] 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 selection-foreground=cdd6f4 selection-background=414356 search-box-no-match=11111b f38ba8 search-box-match=cdd6f4 313244 jump-labels=11111b fab387 urls=89b4fa foot-1.21.0/themes/chiba-dark000066400000000000000000000010101476600145200157340ustar00rootroot00000000000000# -*- conf -*- # theme: Chiba Dark # author: ayushnix (https://sr.ht/~ayushnix) # description: A dark theme with bright cyberpunk colors (WCAG AAA compliant) [cursor] color = 181818 cdcdcd [colors] 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 foot-1.21.0/themes/derp000066400000000000000000000005321476600145200147110ustar00rootroot00000000000000# -*- conf -*- # Derp [cursor] color=000000 ffffff [colors] foreground=ffffff background=000000 regular0=111111 regular1=d36265 regular2=aece91 regular3=e7e18c regular4=5297cf regular5=963c59 regular6=5e7175 regular7=bebebe bright0=666666 bright1=ef8171 bright2=cfefb3 bright3=fff796 bright4=74b8ef bright5=b85e7b bright6=a3babf bright7=ffffff foot-1.21.0/themes/deus000066400000000000000000000010651476600145200147210ustar00rootroot00000000000000# -*- conf -*- # Deus # Color palette based on: https://github.com/ajmwagar/vim-deus [cursor] color=2c323b eaeaea [colors] background=2c323b foreground=eaeaea regular0=242a32 regular1=d54e53 regular2=98c379 regular3=e5c07b regular4=83a598 regular5=c678dd regular6=70c0ba regular7=eaeaea bright0=666666 bright1=ec3e45 bright2=90c966 bright3=edbf69 bright4=73ba9f bright5=c858e9 bright6=2bcec2 bright7=ffffff # Enable if prefer Deus colors instead of inverterd fg/bg for # highlighting (mouse selection) # selection-foreground=2c323b # selection-background=eaeaea foot-1.21.0/themes/dracula000066400000000000000000000010521476600145200153700ustar00rootroot00000000000000# -*- conf -*- # Dracula [cursor] color=282a36 f8f8f2 [colors] foreground=f8f8f2 background=282a36 regular0=000000 # black regular1=ff5555 # red regular2=50fa7b # green regular3=f1fa8c # yellow regular4=bd93f9 # blue regular5=ff79c6 # magenta regular6=8be9fd # cyan regular7=bfbfbf # white bright0=4d4d4d # bright black bright1=ff6e67 # bright red bright2=5af78e # bright green bright3=f4f99d # bright yellow bright4=caa9fa # bright blue bright5=ff92d0 # bright magenta bright6=9aedfe # bright cyan bright7=e6e6e6 # bright whitefoot-1.21.0/themes/dracula-iterm000066400000000000000000000010721476600145200165100ustar00rootroot00000000000000# -*- conf -*- # Dracula iTerm2 variant [cursor] color=ffffff bbbbbb [colors] 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 foot-1.21.0/themes/electrophoretic000066400000000000000000000016521476600145200171560ustar00rootroot00000000000000# -*- 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 [cursor] color=ffffff 515151 [colors] 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 foot-1.21.0/themes/gruvbox-dark000066400000000000000000000004771476600145200164020ustar00rootroot00000000000000# -*- conf -*- # Gruvbox [colors] 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 foot-1.21.0/themes/gruvbox-light000066400000000000000000000005071476600145200165620ustar00rootroot00000000000000# -*- conf -*- # Gruvbox - Light [colors] 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 foot-1.21.0/themes/hacktober000066400000000000000000000010601476600145200157160ustar00rootroot00000000000000# -*- conf -*- [cursor] color=141414 c9c9c9 [colors] foreground=c9c9c9 background=141414 regular0=191918 # black regular1=b34538 # red regular2=587744 # green regular3=d08949 # yellow regular4=206ec5 # blue regular5=864651 # magenta regular6=ac9166 # cyan regular7=f1eee7 # white bright0=2c2b2a # bright black bright1=b33323 # bright red bright2=42824a # bright green bright3=c75a22 # bright yellow bright4=5389c5 # bright blue bright5=e795a5 # bright magenta bright6=ebc587 # bright cyan bright7=ffffff # bright white foot-1.21.0/themes/iterm000066400000000000000000000013321476600145200150760ustar00rootroot00000000000000# -*- conf -*- # this foot theme is based on alacritty iterm theme: # https://github.com/alacritty/alacritty-theme/blob/master/themes/iterm.toml [colors] 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 foot-1.21.0/themes/jetbrains-darcula000066400000000000000000000013001476600145200173430ustar00rootroot00000000000000# -*- conf -*- # JetBrains Darcula # Palette based on the same theme from https://github.com/dexpota/kitty-themes [cursor] color=202020 ffffff [colors] background=202020 foreground=adadad regular0=000000 # black regular1=fa5355 # red regular2=126e00 # green regular3=c2c300 # yellow regular4=4581eb # blue regular5=fa54ff # magenta regular6=33c2c1 # cyan regular7=adadad # white bright0=545454 # bright black bright1=fb7172 # bright red bright2=67ff4f # bright green bright3=ffff00 # bright yellow bright4=6d9df1 # bright blue bright5=fb82ff # bright magenta bright6=60d3d1 # bright cyan bright7=eeeeee # bright white # selection-foreground=202020 # selection-background=1a3272 foot-1.21.0/themes/kitty000066400000000000000000000010411476600145200151170ustar00rootroot00000000000000# -*- conf -*- [cursor] color=111111 cccccc [colors] foreground=dddddd background=000000 regular0=000000 # black regular1=cc0403 # red regular2=19cb00 # green regular3=cecb00 # yellow regular4=0d73cc # blue regular5=cb1ed1 # magenta regular6=0dcdcd # cyan regular7=dddddd # white bright0=767676 # bright black bright1=f2201f # bright red bright2=23fd00 # bright green bright3=fffd00 # bright yellow bright4=1a8fff # bright blue bright5=fd28ff # bright magenta bright6=14ffff # bright cyan bright7=ffffff # bright white foot-1.21.0/themes/material-amber000066400000000000000000000016101476600145200166370ustar00rootroot00000000000000# -*- conf -*- # Material Amber # Based on material.io guidelines with Amber 50 background [cursor] color=fff8e1 21201d [colors] foreground = 21201d background = fff8e1 regular0 = 21201d # black regular1 = cd4340 # red regular2 = 498d49 # green regular3 = fab32d # yellow regular4 = 3378c4 # blue regular5 = b83269 # magenta regular6 = 21929a # cyan regular7 = ffd7d7 # white bright0 = 66635a # bright black bright1 = dd7b72 # bright red bright2 = 82ae78 # bright green bright3 = fbc870 # bright yellow bright4 = 73a0cd # bright blue bright5 = ce6f8e # bright magenta bright6 = 548c94 # bright cyan bright7 = ffe1da # bright white dim0 = 9e9a8c # dim black dim1 = e9a99b # dim red dim2 = b0c99f # dim green dim3 = fdda9a # dim yellow dim4 = a6c0d4 # dim blue dim5 = e0a1ad # dim magenta dim6 = 3c6064 # dim cyan dim7 = ffe9dd # dim white # selection-foreground=fff8e1 # selection-background=21201d foot-1.21.0/themes/material-design000066400000000000000000000012101476600145200170160ustar00rootroot00000000000000# -*- conf -*- # Material # From https://github.com/MartinSeeler/iterm2-material-design [colors] foreground=ECEFF1 background=263238 regular0=546E7A # black regular1=FF5252 # red regular2=5CF19E # green regular3=FFD740 # yellow regular4=40C4FF # blue regular5=FF4081 # magenta regular6=64FCDA # cyan regular7=FFFFFF # white bright0=B0BEC5 # bright black bright1=FF8A80 # bright red bright2=B9F6CA # bright green bright3=FFE57F # bright yellow bright4=80D8FF # bright blue bright5=FF80AB # bright magenta bright6=A7FDEB # bright cyan bright7=FFFFFF # bright white # selection-foreground=ECEFF1 # selection-background=607D8B foot-1.21.0/themes/modus-operandi000066400000000000000000000005731476600145200167120ustar00rootroot00000000000000# -*- conf -*- # # modus-operandi # See: https://protesilaos.com/emacs/modus-themes # [colors] background=ffffff foreground=000000 regular0=000000 regular1=a60000 regular2=005e00 regular3=813e00 regular4=0031a9 regular5=721045 regular6=00538b regular7=bfbfbf bright0=595959 bright1=972500 bright2=315b00 bright3=70480f bright4=2544bb bright5=5317ac bright6=005a5f bright7=ffffff foot-1.21.0/themes/modus-vivendi000066400000000000000000000005731476600145200165550ustar00rootroot00000000000000# -*- conf -*- # # modus-vivendi # See: https://protesilaos.com/emacs/modus-themes # [colors] background=000000 foreground=ffffff regular0=000000 regular1=ff8059 regular2=44bc44 regular3=d0bc00 regular4=2fafff regular5=feacd0 regular6=00d3d0 regular7=bfbfbf bright0=595959 bright1=ef8b50 bright2=70b900 bright3=c0c530 bright4=79a8ff bright5=b6a0ff bright6=6ae4b9 bright7=ffffff foot-1.21.0/themes/monokai-pro000066400000000000000000000005031476600145200162100ustar00rootroot00000000000000# -*- conf -*- # Monokai Pro [colors] background=2D2A2E foreground=FCFCFA regular0=403E41 regular1=FF6188 regular2=A9DC76 regular3=FFD866 regular4=FC9867 regular5=AB9DF2 regular6=78DCE8 regular7=FCFCFA bright0=727072 bright1=FF6188 bright2=A9DC76 bright3=FFD866 bright4=FC9867 bright5=AB9DF2 bright6=78DCE8 bright7=FCFCFA foot-1.21.0/themes/moonfly000066400000000000000000000007761476600145200154540ustar00rootroot00000000000000# -*- conf -*- # moonfly # Based on https://github.com/bluz71/vim-moonfly-colors [cursor] color = 080808 9e9e9e [colors] foreground = b2b2b2 background = 080808 # selection-foreground = 080808 # selection-background = b2ceee regular0 = 323437 regular1 = ff5454 regular2 = 8cc85f regular3 = e3c78a regular4 = 80a0ff regular5 = d183e8 regular6 = 79dac8 regular7 = c6c6c6 bright0 = 949494 bright1 = ff5189 bright2 = 36c692 bright3 = c2c292 bright4 = 74b2ff bright5 = ae81ff bright6 = 85dc85 bright7 = e4e4e4 foot-1.21.0/themes/neon000066400000000000000000000005411476600145200147160ustar00rootroot00000000000000# # vim: ft=dosini # # Neon # # https://xcolors.net/neon # [colors] 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 foot-1.21.0/themes/nightfly000066400000000000000000000010031476600145200155750ustar00rootroot00000000000000# -*- conf -*- # nightfly # Based on https://github.com/bluz71/vim-nightfly-guicolors [cursor] color = 080808 9ca1aa [colors] foreground = acb4c2 background = 011627 # selection-foreground = 080808 # selection-background = b2ceee regular0 = 1d3b53 regular1 = fc514e regular2 = a1cd5e regular3 = e3d18a regular4 = 82aaff regular5 = c792ea regular6 = 7fdbca regular7 = a1aab8 bright0 = 7c8f8f bright1 = ff5874 bright2 = 21c7a8 bright3 = ecc48d bright4 = 82aaff bright5 = ae81ff bright6 = ae81ff bright7 = d6deeb foot-1.21.0/themes/noirblaze000066400000000000000000000012411476600145200157420ustar00rootroot00000000000000# -*- conf -*- # noirblaze-kitty # https://github.com/n1ghtmare/noirblaze-kitty [cursor] color=121212 ff0088 [colors] 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 foot-1.21.0/themes/nord000066400000000000000000000015421476600145200147230ustar00rootroot00000000000000# -*- conf -*- # theme: Nord # author: Arctic Ice Studio , Sven Greb # description: „Nord“ — An arctic, north-bluish color palette # # this specific foot theme is based on nord-alacritty: # https://github.com/arcticicestudio/nord-alacritty/blob/develop/src/nord.yml [cursor] color = 2e3440 d8dee9 [colors] foreground = d8dee9 background = 2e3440 # selection-foreground = d8dee9 # selection-background = 4c566a regular0 = 3b4252 regular1 = bf616a regular2 = a3be8c regular3 = ebcb8b regular4 = 81a1c1 regular5 = b48ead regular6 = 88c0d0 regular7 = e5e9f0 bright0 = 4c566a bright1 = bf616a bright2 = a3be8c bright3 = ebcb8b bright4 = 81a1c1 bright5 = b48ead bright6 = 8fbcbb bright7 = eceff4 dim0 = 373e4d dim1 = 94545d dim2 = 809575 dim3 = b29e75 dim4 = 68809a dim5 = 8c738c dim6 = 6d96a5 dim7 = aeb3bb foot-1.21.0/themes/nordiq000066400000000000000000000005351476600145200152560ustar00rootroot00000000000000# -*- conf -*- # Nordiq [cursor] color=eeeeee 9f515a [colors] foreground=dbdee9 background=0e1420 regular0=5b6272 regular1=bf616a regular2=a3be8c regular3=ebcb8b regular4=81a1c1 regular5=b48ead regular6=88c0d0 regular7=e5e9f0 bright0=4c566a bright1=bf616a bright2=a3be8c bright3=ebcb8b bright4=81a1c1 bright5=b48ead bright6=8fbcbb bright7=eceff4 foot-1.21.0/themes/nvim-dark000066400000000000000000000017101476600145200156460ustar00rootroot00000000000000# -*- 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 [cursor] color=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2 [colors] 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 foot-1.21.0/themes/nvim-light000066400000000000000000000016761476600145200160470ustar00rootroot00000000000000# -*- 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 [cursor] color=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2 [colors] 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 foot-1.21.0/themes/onedark000066400000000000000000000012671476600145200154100ustar00rootroot00000000000000# OneDark # Palette based on the same theme from https://github.com/dexpota/kitty-themes [cursor] color=111111 cccccc [colors] foreground=979eab background=282c34 regular0=282c34 # black regular1=e06c75 # red regular2=98c379 # green regular3=e5c07b # yellow regular4=61afef # blue regular5=be5046 # magenta regular6=56b6c2 # cyan regular7=979eab # white bright0=393e48 # bright black bright1=d19a66 # bright red bright2=56b6c2 # bright green bright3=e5c07b # bright yellow bright4=61afef # bright blue bright5=be5046 # bright magenta bright6=56b6c2 # bright cyan bright7=abb2bf # bright white # selection-foreground=282c34 # selection-background=979eab foot-1.21.0/themes/onehalf-dark000066400000000000000000000021351476600145200163130ustar00rootroot00000000000000# theme: One Half - dark version # author: Son A. Pham # repo: https://github.com/sonph/onehalf # # foot theme is based mainly on values from this specific file: # https://github.com/sonph/onehalf/blob/master/kitty/onehalf-dark.conf # + cursor colors from: # https://github.com/sonph/onehalf/blob/master/iterm/OneHalfDark.itermcolors [cursor] color=dcdfe4 a3b3cc [colors] 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 foot-1.21.0/themes/panda000066400000000000000000000011751476600145200150460ustar00rootroot00000000000000# -*- conf -*- # http://panda.siamak.me/ [colors] # 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 whitefoot-1.21.0/themes/paper-color-dark000066400000000000000000000012571476600145200171260ustar00rootroot00000000000000# -*- conf -*- # PaperColorDark # Palette based on https://github.com/NLKNguyen/papercolor-theme [cursor] color=1c1c1c eeeeee [colors] 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 foot-1.21.0/themes/paper-color-light000066400000000000000000000012611476600145200173070ustar00rootroot00000000000000# -*- conf -*- # PaperColor Light # Palette based on https://github.com/NLKNguyen/papercolor-theme [cursor] color=eeeeee 444444 [colors] 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 foot-1.21.0/themes/poimandres000066400000000000000000000007511476600145200161230ustar00rootroot00000000000000# Based on Poimandres color theme for kitti terminal emulator # https://github.com/ubmit/poimandres-kitty [cursor] color=1b1e28 ffffff [colors] 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 foot-1.21.0/themes/rezza000066400000000000000000000020541476600145200151130ustar00rootroot00000000000000# -*- conf -*- # theme: rezza # author: Doug Whiteley (rezza) # original URL: http://metawire.org/~rezza/index.php # currently available via: https://web.archive.org/web/20060207133656/http://metawire.org/~rezza/index.php?page=Configs # this palette was also posted here: # https://bbs.archlinux.org/viewtopic.php?id=17322 # # more description: # original colors are similar to `phrakture` (https://github.com/f4cket/everycolorschemeivefound/blob/master/phrakture) # Note: there is also a slightly modified (earlier?) version (also called rezza) which can be found here: # https://sheet.shiar.nl/source/data/termcol-xcolor.inc.pl # and also posted here: # https://forums.debian.net/viewtopic.php?t=29981 [colors] foreground = cccccc background = 191911 # Normal colors regular0 = 222222 regular1 = 803232 regular2 = 5b762f regular3 = aa9943 regular4 = 324c80 regular5 = 706c9a regular6 = 92b19e regular7 = ffffff # Bright colors bright0 = 222222 bright1 = 982b2b bright2 = 89b83f bright3 = efef60 bright4 = 2b4f98 bright5 = 826ab1 bright6 = a1cdcd bright7 = dedede foot-1.21.0/themes/rose-pine000066400000000000000000000014771476600145200156710ustar00rootroot00000000000000# -*- conf -*- # Rosé Pine [cursor] color=191724 e0def4 [colors] background=191724 foreground=e0def4 regular0=26233a # black (Overlay) regular1=eb6f92 # red (Love) regular2=9ccfd8 # green (Foam) regular3=f6c177 # yellow (Gold) regular4=31748f # blue (Pine) regular5=c4a7e7 # magenta (Iris) regular6=ebbcba # cyan (Rose) regular7=e0def4 # white (Text) bright0=47435d # bright black (lighter Overlay) bright1=ff98ba # bright red (lighter Love) bright2=c5f9ff # bright green (lighter Foam) bright3=ffeb9e # bright yellow (lighter Gold) bright4=5b9ab7 # bright blue (lighter Pine) bright5=eed0ff # bright magenta (lighter Iris) bright6=ffe5e3 # bright cyan (lighter Rose) bright7=fefcff # bright white (lighter Text) flash=f6c177 # yellow (Gold) foot-1.21.0/themes/rose-pine-dawn000066400000000000000000000015011476600145200166040ustar00rootroot00000000000000# -*- conf -*- # Rosé Pine Dawn [cursor] color=faf4ed 575279 [colors] 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) foot-1.21.0/themes/rose-pine-moon000066400000000000000000000015011476600145200166230ustar00rootroot00000000000000# -*- conf -*- # Rosé Pine Moon [cursor] color=232136 e0def4 [colors] 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) foot-1.21.0/themes/selenized-black000066400000000000000000000006431476600145200170160ustar00rootroot00000000000000# -*- conf -*- # Selenized black [cursor] color = 181818 56d8c9 [colors] background= 181818 foreground= b9b9b9 regular0= 252525 regular1= ed4a46 regular2= 70b433 regular3= dbb32d regular4= 368aeb regular5= eb6eb7 regular6= 3fc5b7 regular7= 777777 bright0= 3b3b3b bright1= ff5e56 bright2= 83c746 bright3= efc541 bright4= 4f9cfe bright5= ff81ca bright6= 56d8c9 bright7= dedede foot-1.21.0/themes/selenized-dark000066400000000000000000000006421476600145200166620ustar00rootroot00000000000000# -*- conf -*- # Selenized dark [cursor] color = 103c48 53d6c7 [colors] 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 foot-1.21.0/themes/selenized-light000066400000000000000000000006411476600145200170470ustar00rootroot00000000000000# -*- conf -*- # Selenized light [cursor] color=fbf3db 00978a [colors] 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 foot-1.21.0/themes/selenized-white000066400000000000000000000006411476600145200170600ustar00rootroot00000000000000# -*- conf -*- # Selenized white [cursor] color=ffffff 009a8a [colors] background= ffffff foreground= 474747 regular0= ebebeb regular1= d6000c regular2= 1d9700 regular3= c49700 regular4= 0064e4 regular5= dd0f9d regular6= 00ad9c regular7= 878787 bright0= cdcdcd bright1= bf0000 bright2= 008400 bright3= af8500 bright4= 0054cf bright5= c7008b bright6= 009a8a bright7= 282828 foot-1.21.0/themes/solarized-dark000066400000000000000000000011001476600145200166620ustar00rootroot00000000000000# -*- conf -*- # Solarized dark [cursor] color= 002b36 93a1a1 [colors] background= 002b36 foreground= 839496 regular0= 073642 regular1= dc322f regular2= 859900 regular3= b58900 regular4= 268bd2 regular5= d33682 regular6= 2aa198 regular7= eee8d5 bright0= 002b36 bright1= cb4b16 bright2= 586e75 bright3= 657b83 bright4= 839496 bright5= 6c71c4 bright6= 93a1a1 bright7= fdf6e3 # Enable if prefer solarized colors instead of inverterd fg/bg for # highlighting (mouse selection) # selection-foreground=93a1a1 # selection-background=073642 foot-1.21.0/themes/solarized-dark-normal-brights000066400000000000000000000011661476600145200216240ustar00rootroot00000000000000# -*- conf -*- # Solarized dark [cursor] color= 002b36 93a1a1 [colors] background= 002b36 foreground= 839496 regular0= 073642 regular1= dc322f regular2= 859900 regular3= b58900 regular4= 268bd2 regular5= d33682 regular6= 2aa198 regular7= eee8d5 # regularN brightened by increasing luminance by 20% bright0= 08404f bright1= e35f5c bright2= 9fb700 bright3= d9a400 bright4= 4ba1de bright5= dc619d bright6= 32c1b6 bright7= ffffff # Enable if prefer solarized colors instead of inverterd fg/bg for # highlighting (mouse selection) # selection-foreground=93a1a1 # selection-background=073642 foot-1.21.0/themes/solarized-light000066400000000000000000000012111476600145200170530ustar00rootroot00000000000000# -*- conf -*- # Solarized light [cursor] color=fdf6e3 586e75 [colors] 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 foot-1.21.0/themes/srcery000066400000000000000000000010141476600145200152620ustar00rootroot00000000000000# srcery [colors] 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 foot-1.21.0/themes/starlight000066400000000000000000000006341476600145200157630ustar00rootroot00000000000000# -*- conf -*- # Theme: starlight V4 (https://github.com/CosmicToast/starlight) [colors] 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 foot-1.21.0/themes/tango000066400000000000000000000005331476600145200150700ustar00rootroot00000000000000# -*- conf -*- # Tango [cursor] color=000000 babdb6 [colors] foreground=babdb6 background=000000 regular0=2e3436 regular1=cc0000 regular2=4e9a06 regular3=c4a000 regular4=3465a4 regular5=75507b regular6=06989a regular7=d3d7cf bright0=555753 bright1=ef2929 bright2=8ae234 bright3=fce94f bright4=729fcf bright5=ad7fa8 bright6=34e2e2 bright7=eeeeec foot-1.21.0/themes/tempus-autumn000066400000000000000000000011431476600145200166020ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Autumn # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by earthly colours (WCAG AA compliant) #[cursor] #color = 302420 a9a2a6 [colors] foreground = a9a2a6 background = 302420 regular0 = 302420 regular1 = f46f55 regular2 = 85a400 regular3 = b09640 regular4 = 799aca regular5 = df798e regular6 = 52a885 regular7 = a8948a bright0 = 36302a bright1 = e27e3d bright2 = 43aa7a bright3 = ba9400 bright4 = 958fdf bright5 = ce7dc4 bright6 = 2fa6b7 bright7 = a9a2a6 # selection-foreground = a8948a # selection-background = 36302a foot-1.21.0/themes/tempus-classic000066400000000000000000000011101476600145200167040ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Classic # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with warm hues (WCAG AA compliant) #[cursor] #color = 232323 aeadaf [colors] foreground = aeadaf background = 232323 regular0 = 232323 regular1 = d4823d regular2 = 8c9e3d regular3 = b1942b regular4 = 6e9cb0 regular5 = b58d88 regular6 = 6da280 regular7 = 949d9f bright0 = 312e30 bright1 = d0913d bright2 = 96a42d bright3 = a8a030 bright4 = 8e9cc0 bright5 = d58888 bright6 = 7aa880 bright7 = aeadaf # selection-foreground = 949d9f # selection-background = 312e30 foot-1.21.0/themes/tempus-dawn000066400000000000000000000011411476600145200162200ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Dawn # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme with a soft, slightly desaturated palette (WCAG AA compliant) #[cursor] #color = eff0f2 4a4b4e [colors] foreground = 4a4b4e background = eff0f2 regular0 = 4a4b4e regular1 = a32a3a regular2 = 206620 regular3 = 745300 regular4 = 4b529a regular5 = 8d377e regular6 = 086784 regular7 = dee2e0 bright0 = 676364 bright1 = a64822 bright2 = 187408 bright3 = 8b590a bright4 = 5c59b2 bright5 = 8e45a8 bright6 = 3f649c bright7 = eff0f2 # selection-foreground = 676364 # selection-background = dee2e0 foot-1.21.0/themes/tempus-day000066400000000000000000000011101476600145200160400ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Day # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme with warm colours (WCAG AA compliant) #[cursor] #color = f8f2e5 464340 [colors] foreground = 464340 background = f8f2e5 regular0 = 464340 regular1 = c81000 regular2 = 107410 regular3 = 806000 regular4 = 385dc4 regular5 = b63052 regular6 = 007070 regular7 = e7e3d7 bright0 = 68607d bright1 = b24000 bright2 = 427040 bright3 = 6f6600 bright4 = 0f64c4 bright5 = 8050a7 bright6 = 336c87 bright7 = f8f2e5 # selection-foreground = 68607d # selection-background = e7e3d7 foot-1.21.0/themes/tempus-dusk000066400000000000000000000011511476600145200162360ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Dusk # 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] foreground = a2a8ba background = 1f252d regular0 = 1f252d regular1 = cb8d56 regular2 = 8ba089 regular3 = a79c46 regular4 = 8c9abe regular5 = b190af regular6 = 8e9aba regular7 = a29899 bright0 = 2c3150 bright1 = d39d74 bright2 = 80b48f bright3 = bda75a bright4 = 9ca5de bright5 = c69ac6 bright6 = 8caeb6 bright7 = a2a8ba # selection-foreground = a29899 # selection-background = 2c3150 foot-1.21.0/themes/tempus-fugit000066400000000000000000000011551476600145200164120ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Fugit # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light, pleasant theme optimised for long writing/coding sessions (WCAG AA compliant) #[cursor] #color = fff5f3 4d595f [colors] foreground = 4d595f background = fff5f3 regular0 = 4d595f regular1 = c61a14 regular2 = 357200 regular3 = 825e00 regular4 = 1666b0 regular5 = a83884 regular6 = 007072 regular7 = efe6e4 bright0 = 796271 bright1 = b93f1a bright2 = 437520 bright3 = 985900 bright4 = 485adf bright5 = a234c0 bright6 = 00756a bright7 = fff5f3 # selection-foreground = 796271 # selection-background = efe6e4 foot-1.21.0/themes/tempus-future000066400000000000000000000011551476600145200166060ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Future # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with colours inspired by concept art of outer space (WCAG AAA compliant) #[cursor] #color = 090a18 b4abac [colors] foreground = b4abac background = 090a18 regular0 = 090a18 regular1 = ff7e8f regular2 = 6aba39 regular3 = bfa51a regular4 = 4ab2d7 regular5 = e58f84 regular6 = 2ab7bb regular7 = a7a2c4 bright0 = 2b1329 bright1 = f78e2f bright2 = 60ba80 bright3 = de9b1d bright4 = 8ba7ea bright5 = e08bd6 bright6 = 2cbab6 bright7 = b4abac # selection-foreground = a7a2c4 # selection-background = 2b1329 foot-1.21.0/themes/tempus-night000066400000000000000000000011321476600145200164000ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Night # author: Protesilaos Stavrou (https://protesilaos.com) # description: High contrast dark theme with bright colours (WCAG AAA compliant) #[cursor] #color = 1a1a1a e0e0e0 [colors] foreground = e0e0e0 background = 1a1a1a regular0 = 1a1a1a regular1 = ff929f regular2 = 5fc940 regular3 = c5b300 regular4 = 5fb8ff regular5 = ef91df regular6 = 1dc5c3 regular7 = c4bdaf bright0 = 242536 bright1 = f69d6a bright2 = 88c400 bright3 = d7ae00 bright4 = 8cb4f0 bright5 = de99f0 bright6 = 00ca9a bright7 = e0e0e0 # selection-foreground = c4bdaf # selection-background = 242536 foot-1.21.0/themes/tempus-past000066400000000000000000000011351476600145200162410ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Past # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme inspired by old vaporwave concept art (WCAG AA compliant) #[cursor] #color = f3f2f4 53545b [colors] foreground = 53545b background = f3f2f4 regular0 = 53545b regular1 = c00c50 regular2 = 0a7040 regular3 = a6403a regular4 = 1763aa regular5 = b02874 regular6 = 096a83 regular7 = eae2de bright0 = 80565d bright1 = bd3133 bright2 = 337243 bright3 = 8d554a bright4 = 5559bb bright5 = b022a7 bright6 = 07707a bright7 = f3f2f4 # selection-foreground = 80565d # selection-background = eae2de foot-1.21.0/themes/tempus-rift000066400000000000000000000011571476600145200162420ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Rift # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a subdued palette on the green side of the spectrum (WCAG AA compliant) #[cursor] #color = 162c22 bbbcbc [colors] foreground = bbbcbc background = 162c22 regular0 = 162c22 regular1 = c19904 regular2 = 34b534 regular3 = 7fad00 regular4 = 30aeb0 regular5 = c8954c regular6 = 5fad8f regular7 = ab9aa9 bright0 = 283431 bright1 = d2a634 bright2 = 6ac134 bright3 = 82bd00 bright4 = 56bdad bright5 = cca0ba bright6 = 10c480 bright7 = bbbcbc # selection-foreground = ab9aa9 # selection-background = 283431 foot-1.21.0/themes/tempus-spring000066400000000000000000000011501476600145200165710ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Spring # 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] foreground = b5b8b7 background = 283a37 regular0 = 283a37 regular1 = ff8b5f regular2 = 5ec04d regular3 = b0b01a regular4 = 39bace regular5 = e99399 regular6 = 36c08e regular7 = 99afae bright0 = 2a453d bright1 = e19e00 bright2 = 73be0d bright3 = c6a843 bright4 = 70afef bright5 = d095e2 bright6 = 3cbfaf bright7 = b5b8b7 # selection-foreground = 99afae # selection-background = 2a453d foot-1.21.0/themes/tempus-summer000066400000000000000000000011541476600145200166030ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Summer # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with colours inspired by summer evenings by the sea (WCAG AA compliant) #[cursor] #color = 202c3d a0abae [colors] foreground = a0abae background = 202c3d regular0 = 202c3d regular1 = fe6f70 regular2 = 4eb075 regular3 = ba9a0a regular4 = 60a1e6 regular5 = d285ad regular6 = 3dae9f regular7 = 949cbf bright0 = 39304f bright1 = ec7f4f bright2 = 5baf4f bright3 = be981f bright4 = 8599ef bright5 = cc82d7 bright6 = 2aacbf bright7 = a0abae # selection-foreground = 949cbf # selection-background = 39304f foot-1.21.0/themes/tempus-tempest000066400000000000000000000011421476600145200167510ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Tempest # author: Protesilaos Stavrou (https://protesilaos.com) # description: A green-scale, subtle theme for late night hackers (WCAG AAA compliant) #[cursor] #color = 282b2b b6e0ca [colors] foreground = b6e0ca background = 282b2b regular0 = 282b2b regular1 = cfc80a regular2 = 7ad97a regular3 = bfcc4a regular4 = 60d7cd regular5 = c5c4af regular6 = 8bd0bf regular7 = b0c8ca bright0 = 323535 bright1 = d1d933 bright2 = 99e299 bright3 = bbde4f bright4 = 74e4cd bright5 = d2d4aa bright6 = 9bdfc4 bright7 = b6e0ca # selection-foreground = b0c8ca # selection-background = 323535 foot-1.21.0/themes/tempus-totus000066400000000000000000000011421476600145200164460ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Totus # author: Protesilaos Stavrou (https://protesilaos.com) # description: Light theme for prose or for coding in an open space (WCAG AAA compliant) #[cursor] #color = ffffff 4a484d [colors] foreground = 4a484d background = ffffff regular0 = 4a484d regular1 = a50000 regular2 = 005d26 regular3 = 714700 regular4 = 1d3ccf regular5 = 88267a regular6 = 185570 regular7 = efefef bright0 = 5e4b4f bright1 = 992030 bright2 = 4a5500 bright3 = 8a3600 bright4 = 2d45b0 bright5 = 700dc9 bright6 = 005289 bright7 = ffffff # selection-foreground = 5e4b4f # selection-background = efefef foot-1.21.0/themes/tempus-warp000066400000000000000000000011151476600145200162410ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Warp # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a vibrant palette (WCAG AA compliant) #[cursor] #color = 001514 a29fa0 [colors] foreground = a29fa0 background = 001514 regular0 = 001514 regular1 = ff3737 regular2 = 169c16 regular3 = 9f8500 regular4 = 5781ef regular5 = da4ebf regular6 = 009880 regular7 = 968282 bright0 = 261c2c bright1 = F0681A bright2 = 3aa73a bright3 = ba8a00 bright4 = 8887f0 bright5 = d85cf2 bright6 = 1da1af bright7 = a29fa0 # selection-foreground = 968282 # selection-background = 261c2c foot-1.21.0/themes/tempus-winter000066400000000000000000000011551476600145200166040ustar00rootroot00000000000000# -*- conf -*- # theme: Tempus Winter # author: Protesilaos Stavrou (https://protesilaos.com) # description: Dark theme with a palette inspired by winter nights at the city (WCAG AA compliant) #[cursor] #color = 202427 8da3b8 [colors] foreground = 8da3b8 background = 202427 regular0 = 202427 regular1 = ed6e5a regular2 = 4aa920 regular3 = 9a9921 regular4 = 7b91df regular5 = d17e80 regular6 = 4fa394 regular7 = 91959b bright0 = 2a2e38 bright1 = de7b28 bright2 = 00ab5f bright3 = af9155 bright4 = 329fcb bright5 = ca77c5 bright6 = 1ba6a4 bright7 = 8da3b8 # selection-foreground = 91959b # selection-background = 2a2e38 foot-1.21.0/themes/tokyonight-light000066400000000000000000000007541476600145200172710ustar00rootroot00000000000000# -*- conf -*- # Reference: https://github.com/tokyo-night/tokyo-night-vscode-theme/blob/master/themes/tokyo-night-light-color-theme.json [colors] 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 foot-1.21.0/themes/tokyonight-night000066400000000000000000000004651476600145200172720ustar00rootroot00000000000000# -*- conf -*- [colors] background=1a1b26 foreground=c0caf5 regular0=15161E regular1=f7768e regular2=9ece6a regular3=e0af68 regular4=7aa2f7 regular5=bb9af7 regular6=7dcfff regular7=a9b1d6 bright0=414868 bright1=f7768e bright2=9ece6a bright3=e0af68 bright4=7aa2f7 bright5=bb9af7 bright6=7dcfff bright7=c0caf5 foot-1.21.0/themes/tokyonight-storm000066400000000000000000000004651476600145200173250ustar00rootroot00000000000000# -*- conf -*- [colors] background=24283b foreground=c0caf5 regular0=1D202F regular1=f7768e regular2=9ece6a regular3=e0af68 regular4=7aa2f7 regular5=bb9af7 regular6=7dcfff regular7=a9b1d6 bright0=414868 bright1=f7768e bright2=9ece6a bright3=e0af68 bright4=7aa2f7 bright5=bb9af7 bright6=7dcfff bright7=c0caf5 foot-1.21.0/themes/visibone000066400000000000000000000005361476600145200156010ustar00rootroot00000000000000# -*- conf -*- # VisiBone [cursor] color=010101 ffffff [colors] foreground=ffffff background=010101 regular0=666666 regular1=cc6666 regular2=66cc99 regular3=cc9966 regular4=6699cc regular5=cc6699 regular6=66cccc regular7=cccccc bright0=999999 bright1=ff9999 bright2=99ffcc bright3=ffcc99 bright4=99ccff bright5=ff99cc bright6=99ffff bright7=ffffff foot-1.21.0/themes/xterm000066400000000000000000000010231476600145200151120ustar00rootroot00000000000000# -*- conf -*- # The default palette of xterm. [colors] 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 foot-1.21.0/themes/zenburn000066400000000000000000000011301476600145200154350ustar00rootroot00000000000000# -*- conf -*- [colors] foreground=dcdccc background=111111 ## Normal/regular colors (color palette 0-7) regular0=222222 # black regular1=cc9393 # red regular2=7f9f7f # green regular3=d0bf8f # yellow regular4=6ca0a3 # blue regular5=dc8cc3 # magenta regular6=93e0e3 # cyan regular7=dcdccc # white ## Bright colors (color palette 8-15) bright0=666666 # bright black bright1=dca3a3 # bright red bright2=bfebbf # bright green bright3=f0dfaf # bright yellow bright4=8cd0d3 # bright blue bright5=fcace3 # bright magenta bright6=b3ffff # bright cyan bright7=ffffff # bright white foot-1.21.0/tokenize.c000066400000000000000000000046751476600145200145570ustar00rootroot00000000000000#include "tokenize.h" #include #include #define LOG_MODULE "tokenize" #define LOG_ENABLE_DBG 0 #include "log.h" #include "xmalloc.h" static bool push_argv(char ***argv, size_t *size, const char *arg, size_t len, size_t *argc) { if (arg != NULL && arg[0] == '%') return true; if (*argc >= *size) { size_t new_size = *size > 0 ? 2 * *size : 10; char **new_argv = realloc(*argv, new_size * sizeof(new_argv[0])); if (new_argv == NULL) return false; *argv = new_argv; *size = new_size; } (*argv)[(*argc)++] = arg != NULL ? xstrndup(arg, len) : NULL; return true; } bool tokenize_cmdline(const char *cmdline, char ***argv) { *argv = NULL; size_t argv_size = 0; const char *final_end = cmdline + strlen(cmdline) + 1; bool first_token_is_quoted = cmdline[0] == '"' || cmdline[0] == '\''; char delim = first_token_is_quoted ? cmdline[0] : ' '; const char *p = first_token_is_quoted ? &cmdline[1] : &cmdline[0]; const char *search_start = p; size_t idx = 0; while (*p != '\0') { char *end = strchr(search_start, delim); if (end == NULL) { if (delim != ' ') { LOG_ERR("unterminated %s quote", delim == '"' ? "double" : "single"); goto err; } if (!push_argv(argv, &argv_size, p, final_end - p, &idx) || !push_argv(argv, &argv_size, NULL, 0, &idx)) { goto err; } else return true; } if (end > p && *(end - 1) == '\\') { /* Escaped quote, remove one level of escaping and * continue searching for "our" closing quote */ memmove(end - 1, end, strlen(end)); end[strlen(end) - 1] = '\0'; search_start = end; continue; } //*end = '\0'; if (!push_argv(argv, &argv_size, p, end - p, &idx)) goto err; p = end + 1; while (*p == delim) p++; while (*p == ' ') p++; if (*p == '"' || *p == '\'') { delim = *p; p++; } else delim = ' '; search_start = p; } if (!push_argv(argv, &argv_size, NULL, 0, &idx)) goto err; return true; err: for (size_t i = 0; i < idx; i++) free((*argv)[i]); free(*argv); *argv = NULL; return false; } foot-1.21.0/tokenize.h000066400000000000000000000001361476600145200145500ustar00rootroot00000000000000#pragma once #include bool tokenize_cmdline(const char *cmdline, char ***argv); foot-1.21.0/unicode-mode.c000066400000000000000000000053411476600145200152660ustar00rootroot00000000000000#include "unicode-mode.h" #define LOG_MODULE "unicode-input" #define LOG_ENABLE_DBG 0 #include "log.h" #include "render.h" #include "search.h" void unicode_mode_activate(struct terminal *term) { if (term->unicode_mode.active) return; term->unicode_mode.active = true; term->unicode_mode.character = u'\0'; term->unicode_mode.count = 0; unicode_mode_updated(term); } void unicode_mode_deactivate(struct terminal *term) { if (!term->unicode_mode.active) return; term->unicode_mode.active = false; unicode_mode_updated(term); } void unicode_mode_updated(struct terminal *term) { if (term == NULL) return; if (term->is_searching) render_refresh_search(term); else render_refresh(term); } void unicode_mode_input(struct seat *seat, struct terminal *term, xkb_keysym_t sym) { if (sym == XKB_KEY_Return || sym == XKB_KEY_space || sym == XKB_KEY_KP_Enter || sym == XKB_KEY_KP_Space) { char utf8[MB_CUR_MAX]; size_t chars = c32rtomb( utf8, term->unicode_mode.character, &(mbstate_t){0}); LOG_DBG("Unicode input: 0x%06x -> %.*s", term->unicode_mode.character, (int)chars, utf8); if (chars != (size_t)-1) { if (term->is_searching) search_add_chars(term, utf8, chars); else term_to_slave(term, utf8, chars); } unicode_mode_deactivate(term); } else if (sym == XKB_KEY_Escape || sym == XKB_KEY_q || (seat->kbd.ctrl && (sym == XKB_KEY_c || sym == XKB_KEY_d || sym == XKB_KEY_g))) { unicode_mode_deactivate(term); } else if (sym == XKB_KEY_BackSpace) { if (term->unicode_mode.count > 0) { term->unicode_mode.character >>= 4; term->unicode_mode.count--; unicode_mode_updated(term); } } else if (term->unicode_mode.count < 6) { int digit = -1; /* 0-9, a-f, A-F */ if (sym >= XKB_KEY_0 && sym <= XKB_KEY_9) digit = sym - XKB_KEY_0; else if (sym >= XKB_KEY_KP_0 && sym <= XKB_KEY_KP_9) digit = sym - XKB_KEY_KP_0; else if (sym >= XKB_KEY_a && sym <= XKB_KEY_f) digit = 0xa + (sym - XKB_KEY_a); else if (sym >= XKB_KEY_A && sym <= XKB_KEY_F) digit = 0xa + (sym - XKB_KEY_A); if (digit >= 0) { xassert(digit >= 0 && digit <= 0xf); term->unicode_mode.character <<= 4; term->unicode_mode.character |= digit; term->unicode_mode.count++; unicode_mode_updated(term); } } } foot-1.21.0/unicode-mode.h000066400000000000000000000005261476600145200152730ustar00rootroot00000000000000#pragma once #include #include "terminal.h" void unicode_mode_activate(struct terminal *term); void unicode_mode_deactivate(struct terminal *term); void unicode_mode_updated(struct terminal *term); void unicode_mode_input(struct seat *seat, struct terminal *term, xkb_keysym_t sym); foot-1.21.0/unicode/000077500000000000000000000000001476600145200141755ustar00rootroot00000000000000foot-1.21.0/unicode/emoji-variation-sequences.txt000066400000000000000000001127421476600145200220330ustar00rootroot00000000000000# 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 foot-1.21.0/uri.c000066400000000000000000000174101476600145200135150ustar00rootroot00000000000000#include "uri.h" #include #include #include #include #define LOG_MODULE "uri" #define LOG_ENABLE_DBG 0 #include "log.h" #include "debug.h" #include "util.h" #include "xmalloc.h" bool uri_parse(const char *uri, size_t len, char **scheme, char **user, char **password, char **host, uint16_t *port, char **path, char **query, char **fragment) { LOG_DBG("parse URI: \"%.*s\"", (int)len, uri); if (scheme != NULL) *scheme = NULL; if (user != NULL) *user = NULL; if (password != NULL) *password = NULL; if (host != NULL) *host = NULL; if (port != NULL) *port = 0; if (path != NULL) *path = NULL; if (query != NULL) *query = NULL; if (fragment != NULL) *fragment = NULL; size_t left = len; const char *start = uri; const char *end = NULL; if ((end = memchr(start, ':', left)) == NULL) goto err; size_t scheme_len = end - start; if (scheme_len == 0) goto err; if (scheme != NULL) *scheme = xstrndup(start, scheme_len); LOG_DBG("scheme: \"%.*s\"", (int)scheme_len, start); start = end + 1; left = len - (start - uri); /* Authinfo */ if (left >= 2 && start[0] == '/' && start[1] == '/') { start += 2; left -= 2; /* [user[:password]@]@host[:port] */ /* Find beginning of path segment (required component * following the authinfo) */ const char *path_segment = memchr(start, '/', left); if (path_segment == NULL) goto err; size_t auth_left = path_segment - start; /* Do we have a user (and optionally a password)? */ const char *user_pw_end = memchr(start, '@', auth_left); if (user_pw_end != NULL) { size_t user_pw_len = user_pw_end - start; /* Do we have a password? */ const char *user_end = memchr(start, ':', user_pw_end - start); if (user_end != NULL) { size_t user_len = user_end - start; if (user_len == 0) goto err; if (user != NULL) *user = xstrndup(start, user_len); const char *pw = user_end + 1; size_t pw_len = user_pw_end - pw; if (pw_len == 0) goto err; if (password != NULL) *password = xstrndup(pw, pw_len); LOG_DBG("user: \"%.*s\"", (int)user_len, start); LOG_DBG("password: \"%.*s\"", (int)pw_len, pw); } else { size_t user_len = user_pw_end - start; if (user_len == 0) goto err; if (user != NULL) *user = xstrndup(start, user_len); LOG_DBG("user: \"%.*s\"", (int)user_len, start); } start = user_pw_end + 1; left = len - (start - uri); auth_left -= user_pw_len + 1; } const char *host_end = memchr(start, ':', auth_left); if (host_end != NULL) { size_t host_len = host_end - start; if (host != NULL) *host = xstrndup(start, host_len); const char *port_str = host_end + 1; size_t port_len = path_segment - port_str; if (port_len == 0) goto err; uint16_t _port = 0; for (size_t i = 0; i < port_len; i++) { if (!(port_str[i] >= '0' && port_str[i] <= '9')) goto err; _port *= 10; _port += port_str[i] - '0'; } if (port != NULL) *port = _port; LOG_DBG("host: \"%.*s\"", (int)host_len, start); LOG_DBG("port: \"%.*s\" (%hu)", (int)port_len, port_str, _port); } else { size_t host_len = path_segment - start; if (host != NULL) *host = xstrndup(start, host_len); LOG_DBG("host: \"%.*s\"", (int)host_len, start); } start = path_segment; left = len - (start - uri); } /* Do we have a query? */ const char *query_start = memchr(start, '?', left); const char *fragment_start = memchr(start, '#', left); if (streq(*scheme, "file")) { /* Don't try to parse query/fragment in file URIs, just treat the remaining text as path */ query_start = NULL; fragment_start = NULL; } else if (query_start != NULL && fragment_start != NULL && fragment_start < query_start) { /* Invalid URI - for now, ignore, and treat is as part of path */ query_start = NULL; fragment_start = NULL; } size_t path_len = query_start != NULL ? query_start - start : fragment_start != NULL ? fragment_start - start : left; if (path_len == 0) goto err; /* Path - decode %xx encoded characters */ if (path != NULL) { const char *encoded = start; char *decoded = xmalloc(path_len + 1); char *p = decoded; size_t encoded_len = path_len; size_t UNUSED decoded_len = 0; while (true) { /* Find next '%' */ const char *next = memchr(encoded, '%', encoded_len); if (next == NULL) { strncpy(p, encoded, encoded_len); decoded_len += encoded_len; p += encoded_len; break; } /* Copy everything leading up to the '%' */ size_t prefix_len = next - encoded; memcpy(p, encoded, prefix_len); p += prefix_len; encoded_len -= prefix_len; decoded_len += prefix_len; if (hex2nibble(next[1]) <= 15 && hex2nibble(next[2]) <= 15) { *p++ = hex2nibble(next[1]) << 4 | hex2nibble(next[2]); decoded_len++; encoded_len -= 3; encoded = next + 3; } else { *p++ = *next; decoded_len++; encoded_len -= 1; encoded = next + 1; } } *p = '\0'; *path = decoded; LOG_DBG("path: encoded=\"%.*s\", decoded=\"%s\"", (int)path_len, start, decoded); } else LOG_DBG("path: encoded=\"%.*s\", decoded=", (int)path_len, start); start = query_start != NULL ? query_start + 1 : fragment_start != NULL ? fragment_start + 1 : uri + len; left = len - (start - uri); if (query_start != NULL) { size_t query_len = fragment_start != NULL ? fragment_start - start : left; if (query_len == 0) goto err; if (query != NULL) *query = xstrndup(start, query_len); LOG_DBG("query: \"%.*s\"", (int)query_len, start); start = fragment_start != NULL ? fragment_start + 1 : uri + len; left = len - (start - uri); } if (fragment_start != NULL) { if (left == 0) goto err; if (fragment != NULL) *fragment = xstrndup(start, left); LOG_DBG("fragment: \"%.*s\"", (int)left, start); } return true; err: if (scheme != NULL) free(*scheme); if (user != NULL) free(*user); if (password != NULL) free(*password); if (host != NULL) free(*host); if (path != NULL) free(*path); if (query != NULL) free(*query); if (fragment != NULL) free(*fragment); return false; } bool hostname_is_localhost(const char *hostname) { char this_host[_POSIX_HOST_NAME_MAX]; if (gethostname(this_host, sizeof(this_host)) < 0) this_host[0] = '\0'; return (hostname != NULL && ( streq(hostname, "") || streq(hostname, "localhost") || streq(hostname, this_host))); } foot-1.21.0/uri.h000066400000000000000000000005001476600145200135120ustar00rootroot00000000000000#pragma once #include #include #include bool uri_parse(const char *uri, size_t len, char **scheme, char **user, char **password, char **host, uint16_t *port, char **path, char **query, char **fragment); bool hostname_is_localhost(const char *hostname); foot-1.21.0/url-mode.c000066400000000000000000000551731476600145200144520ustar00rootroot00000000000000#include "url-mode.h" #include #include #include #include #include #include #include #define LOG_MODULE "url-mode" #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" #include "grid.h" #include "key-binding.h" #include "quirks.h" #include "render.h" #include "selection.h" #include "spawn.h" #include "terminal.h" #include "uri.h" #include "util.h" #include "xmalloc.h" static void url_destroy(struct url *url); static bool execute_binding(struct seat *seat, struct terminal *term, const struct key_binding *binding, uint32_t serial) { const enum bind_action_url action = binding->action; switch (action) { case BIND_ACTION_URL_NONE: return false; case BIND_ACTION_URL_CANCEL: urls_reset(term); return true; case BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL: term->urls_show_uri_on_jump_label = !term->urls_show_uri_on_jump_label; render_refresh_urls(term); return true; case BIND_ACTION_URL_COUNT: return false; } return true; } static bool spawn_url_launcher_with_token(struct terminal *term, const char *url, const char *xdg_activation_token) { size_t argc; char **argv; int dev_null = open("/dev/null", O_RDWR); if (dev_null < 0) { LOG_ERRNO("failed to open /dev/null"); return false; } xassert(term->url_launch != NULL); bool ret = false; if (spawn_expand_template( term->url_launch, 2, (const char *[]){"url", "match"}, (const char *[]){url, url}, &argc, &argv)) { ret = spawn( term->reaper, term->cwd, argv, dev_null, dev_null, dev_null, NULL, NULL, xdg_activation_token) >= 0; for (size_t i = 0; i < argc; i++) free(argv[i]); free(argv); } term->url_launch = NULL; close(dev_null); return ret; } struct spawn_activation_context { struct terminal *term; char *url; }; static void activation_token_done(const char *token, void *data) { struct spawn_activation_context *ctx = data; spawn_url_launcher_with_token(ctx->term, ctx->url, token); free(ctx->url); free(ctx); } static bool spawn_url_launcher(struct seat *seat, struct terminal *term, const char *url, uint32_t serial) { xassert(term->url_launch != NULL); struct spawn_activation_context *ctx = xmalloc(sizeof(*ctx)); *ctx = (struct spawn_activation_context){ .term = term, .url = xstrdup(url), }; if (wayl_get_activation_token( seat->wayl, seat, serial, term->window, &activation_token_done, ctx)) { /* Context free:d by callback */ return true; } free(ctx->url); free(ctx); return spawn_url_launcher_with_token(term, url, NULL); } static void activate_url(struct seat *seat, struct terminal *term, const struct url *url, uint32_t serial) { char *url_string = NULL; char *scheme, *host, *path; if (uri_parse(url->url, strlen(url->url), &scheme, NULL, NULL, &host, NULL, &path, NULL, NULL)) { if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { /* * This is a file in *this* computer. Pass only the * filename to the URL-launcher. * * I.e. strip the ‘file://user@host/’ prefix. */ url_string = path; } else free(path); free(scheme); free(host); } if (url_string == NULL) url_string = xstrdup(url->url); switch (url->action) { case URL_ACTION_COPY: if (text_to_clipboard(seat, term, url_string, seat->kbd.serial)) { /* Now owned by our clipboard “manager” */ url_string = NULL; } break; case URL_ACTION_LAUNCH: case URL_ACTION_PERSISTENT: { spawn_url_launcher(seat, term, url_string, serial); break; } } free(url_string); } void urls_input(struct seat *seat, struct terminal *term, const struct key_binding_set *bindings, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, const xkb_keysym_t *raw_syms, size_t raw_count, uint32_t serial) { /* * Key bindings */ /* Match untranslated symbols */ tll_foreach(bindings->url, it) { const struct key_binding *bind = &it->item; if (bind->mods != mods || bind->mods == 0) continue; for (size_t i = 0; i < raw_count; i++) { if (bind->k.sym == raw_syms[i]) { execute_binding(seat, term, bind, serial); return; } } } /* Match translated symbol */ tll_foreach(bindings->url, it) { const struct key_binding *bind = &it->item; if (bind->k.sym == sym && bind->mods == (mods & ~consumed)) { execute_binding(seat, term, bind, serial); return; } } /* Match raw key code */ tll_foreach(bindings->url, it) { const struct key_binding *bind = &it->item; if (bind->mods != mods || bind->mods == 0) continue; /* Match raw key code */ tll_foreach(bind->k.key_codes, code) { if (code->item == key) { execute_binding(seat, term, bind, serial); return; } } } size_t seq_len = c32len(term->url_keys); if (sym == XKB_KEY_BackSpace) { if (seq_len > 0) { term->url_keys[seq_len - 1] = U'\0'; render_refresh_urls(term); } return; } if (mods & ~consumed) return; char32_t wc = xkb_state_key_get_utf32(seat->kbd.xkb_state, key); /* * Determine if this is a "valid" key. I.e. if there is a URL * label with a key combo where this key is the next in * sequence. */ bool is_valid = false; const struct url *match = NULL; tll_foreach(term->urls, it) { if (it->item.key == NULL) continue; const struct url *url = &it->item; const size_t key_len = c32len(it->item.key); if (key_len >= seq_len + 1 && c32ncasecmp(url->key, term->url_keys, seq_len) == 0 && toc32lower(url->key[seq_len]) == toc32lower(wc)) { is_valid = true; if (key_len == seq_len + 1) { match = url; break; } } } if (match) { activate_url(seat, term, match, serial); switch (match->action) { case URL_ACTION_COPY: case URL_ACTION_LAUNCH: urls_reset(term); break; case URL_ACTION_PERSISTENT: term->url_keys[0] = U'\0'; render_refresh_urls(term); break; } } else if (is_valid) { xassert(seq_len + 1 <= ALEN(term->url_keys)); term->url_keys[seq_len] = wc; render_refresh_urls(term); } } struct vline { char *utf8; size_t len; /* Length of utf8[] */ size_t sz; /* utf8[] allocated size */ struct coord *map; /* Maps utf8[ofs] to grid coordinates */ }; static void regex_detected(const struct terminal *term, enum url_action action, const regex_t *preg, url_list_t *urls) { /* * Use regcomp()+regexec() to find patterns. * * Since we can't feed regexec() one character at a time, and * since it doesn't accept wide characters, we need to build utf8 * strings. * * Each string represents a logical line (i.e. handle line-wrap). * To be able to map regex matches back to the grid, we store the * grid coordinates of *each* character, in the line struct as * well. This is offset based; utf8[ofs] has its grid coordinates * in map[ofs. */ /* There is *at most* term->rows logical lines */ struct vline vlines[term->rows]; size_t vline_idx = 0; memset(vlines, 0, sizeof(vlines)); struct vline *vline = &vlines[vline_idx]; mbstate_t ps = {0}; for (int r = 0; r < term->rows; r++) { const struct row *row = grid_row_in_view(term->grid, r); for (int c = 0; c < term->cols; c++) { const struct cell *cell = &row->cells[c]; const char32_t *wc = &cell->wc; size_t wc_count = 1; /* Expand combining characters */ if (wc[0] >= CELL_COMB_CHARS_LO && wc[0] <= CELL_COMB_CHARS_HI) { const struct composed *composed = composed_lookup(term->composed, wc[0] - CELL_COMB_CHARS_LO); xassert(composed != NULL); wc = composed->chars; wc_count = composed->count; } /* 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: %.*srow/col = %dx%d", matches[1].rm_so, (int)mlen, &search_string[matches[1].rm_so], v->map[start].row, v->map[start].col); tll_push_back( *urls, ((struct url){ .id = (uint64_t)rand() << 32 | rand(), .url = xstrndup(&v->utf8[start], mlen), .range = { .start = v->map[start], .end = v->map[end - 1], /* Inclusive */ }, .action = action, .osc8 = false})); search_string += matches[0].rm_eo; } free(v->utf8); free(v->map); } } static void osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls) { bool dont_touch_url_attr = false; switch (term->conf->url.osc8_underline) { case OSC8_UNDERLINE_URL_MODE: dont_touch_url_attr = false; break; case OSC8_UNDERLINE_ALWAYS: dont_touch_url_attr = true; break; } for (int r = 0; r < term->rows; r++) { const struct row *row = grid_row_in_view(term->grid, r); const struct row_data *extra = row->extra; if (extra == NULL) continue; for (size_t i = 0; i < extra->uri_ranges.count; i++) { const struct row_range *range = &extra->uri_ranges.v[i]; struct coord start = { .col = range->start, .row = r + term->grid->view, }; struct coord end = { .col = range->end, .row = r + term->grid->view, }; tll_push_back( *urls, ((struct url){ .id = range->uri.id, .url = xstrdup(range->uri.uri), .range = { .start = start, .end = end, }, .action = action, .url_mode_dont_change_url_attr = dont_touch_url_attr, .osc8 = true})); } } } static void remove_overlapping(url_list_t *urls, int cols) { tll_foreach(*urls, outer) { tll_foreach(*urls, inner) { if (outer == inner) continue; const struct url *out = &outer->item; const struct url *in = &inner->item; uint64_t in_start = in->range.start.row * cols + in->range.start.col; uint64_t in_end = in->range.end.row * cols + in->range.end.col; uint64_t out_start = out->range.start.row * cols + out->range.start.col; uint64_t out_end = out->range.end.row * cols + out->range.end.col; if ((in_start <= out_start && in_end >= out_start) || (in_start <= out_end && in_end >= out_end) || (in_start >= out_start && in_end <= out_end)) { /* * OSC-8 URLs can't overlap with each * other. * * Similarly, auto-detected URLs cannot overlap with * each other. * * But OSC-8 URLs can overlap with auto-detected ones. */ xassert(in->osc8 || out->osc8); if (in->osc8) outer->item.duplicate = true; else inner->item.duplicate = true; } } } tll_foreach(*urls, it) { if (it->item.duplicate) { url_destroy(&it->item); tll_remove(*urls, it); } } } void urls_collect(const struct terminal *term, enum url_action action, const regex_t *preg, bool osc8, url_list_t *urls) { xassert(tll_length(term->urls) == 0); if (osc8) osc8_uris(term, action, urls); regex_detected(term, action, preg, urls); remove_overlapping(urls, term->grid->num_cols); } static 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]) { const char32_t *alphabet = conf->url.label_letters; const size_t alphabet_len = c32len(alphabet); size_t hints_count = 1; char32_t **hints = xmalloc(hints_count * sizeof(hints[0])); hints[0] = xc32dup(U""); size_t offset = 0; do { const char32_t *prefix = hints[offset++]; const size_t prefix_len = c32len(prefix); hints = xrealloc(hints, (hints_count + alphabet_len) * sizeof(hints[0])); const char32_t *wc = &alphabet[0]; for (size_t i = 0; i < alphabet_len; i++, wc++) { char32_t *hint = xmalloc((prefix_len + 1 + 1) * sizeof(char32_t)); hints[hints_count + i] = hint; /* Will be reversed later */ hint[0] = *wc; c32cpy(&hint[1], prefix); } hints_count += alphabet_len; } while (hints_count - offset < count); xassert(hints_count - offset >= count); /* Copy slice of 'hints' array to the caller provided array */ for (size_t i = 0; i < hints_count; i++) { if (i >= offset && i < offset + count) combos[i - offset] = hints[i]; else free(hints[i]); } free(hints); /* 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]); for (size_t j = 0; j < len / 2; j++) { char32_t tmp = combos[i][j]; combos[i][j] = combos[i][len - j - 1]; combos[i][len - j - 1] = tmp; } } } void urls_assign_key_combos(const struct config *conf, url_list_t *urls) { const size_t count = tll_length(*urls); if (count == 0) return; char32_t *combos[count]; generate_key_combos(conf, count, combos); size_t combo_idx = 0; tll_foreach(*urls, it) { bool id_already_seen = false; /* Look for already processed URLs where both the URI and the * ID matches */ tll_foreach(*urls, it2) { if (&it->item == &it2->item) break; if (it->item.id == it2->item.id && streq(it->item.url, it2->item.url)) { id_already_seen = true; break; } } if (id_already_seen) continue; /* * Scan previous URLs, and check if *this* URL matches any of * them; if so, reuse the *same* key combo. */ bool url_already_seen = false; tll_foreach(*urls, it2) { if (&it->item == &it2->item) break; if (streq(it->item.url, it2->item.url)) { it->item.key = xc32dup(it2->item.key); url_already_seen = true; break; } } if (!url_already_seen) it->item.key = combos[combo_idx++]; } /* Free combos we didn't use up */ for (size_t i = combo_idx; i < count; i++) free(combos[i]); #if defined(_DEBUG) && LOG_ENABLE_DBG tll_foreach(*urls, it) { if (it->item.key == NULL) continue; char *key = ac32tombs(it->item.key); xassert(key != NULL); LOG_DBG("URL: %s (key=%s, id=%"PRIu64")", it->item.url, key, it->item.id); free(key); } #endif } static void tag_cells_for_url(struct terminal *term, const struct url *url, bool value) { if (url->url_mode_dont_change_url_attr) return; struct grid *grid = term->url_grid_snapshot; xassert(grid != NULL); const struct coord *start = &url->range.start; const struct coord *end = &url->range.end; size_t end_r = end->row & (grid->num_rows - 1); size_t r = start->row & (grid->num_rows - 1); size_t c = start->col; struct row *row = grid->rows[r]; row->dirty = true; while (true) { struct cell *cell = &row->cells[c]; cell->attrs.url = value; cell->attrs.clean = 0; if (r == end_r && c == end->col) break; if (++c >= term->cols) { r = (r + 1) & (grid->num_rows - 1); c = 0; row = grid->rows[r]; if (row == NULL) { /* Un-allocated scrollback. This most likely means a * runaway OSC-8 URL. */ break; } row->dirty = true; } } } void urls_render(struct terminal *term, const struct config_spawn_template *launch) { struct wl_window *win = term->window; if (tll_length(win->term->urls) == 0) return; /* Disable IME while in URL-mode */ if (term_ime_is_enabled(term)) { term->ime_reenable_after_url_mode = true; term_ime_disable(term); } /* Dirty the last cursor, to ensure it is erased */ { struct row *cursor_row = term->render.last_cursor.row; if (cursor_row != NULL) { struct cell *cell = &cursor_row->cells[term->render.last_cursor.col]; cell->attrs.clean = 0; cursor_row->dirty = true; } } term->render.last_cursor.row = NULL; /* Clear scroll damage, to ensure we don't apply it twice (once on * the snapshot:ed grid, and then later again on the real grid) */ tll_free(term->grid->scroll_damage); /* Damage the entire view, to ensure a full screen redraw, both * now, when entering URL mode, and later, when exiting it. */ term_damage_view(term); /* Snapshot the current grid */ term->url_grid_snapshot = grid_snapshot(term->grid); /* Remember which launcher to use */ term->url_launch = launch; xassert(tll_length(win->urls) == 0); tll_foreach(win->term->urls, it) { struct wl_url url = {.url = &it->item}; wayl_win_subsurface_new(win, &url.surf, false); tll_push_back(win->urls, url); tag_cells_for_url(term, &it->item, true); } render_refresh_urls(term); render_refresh(term); } static void url_destroy(struct url *url) { free(url->url); free(url->key); } void urls_reset(struct terminal *term) { if (likely(tll_length(term->urls) == 0)) { xassert(term->url_grid_snapshot == NULL); return; } grid_free(term->url_grid_snapshot); free(term->url_grid_snapshot); term->url_grid_snapshot = NULL; /* * Make sure "last cursor" doesn't point to a row in the just * free:d snapshot grid. * * Note that it will still be erased properly (if hasn't already), * since we marked the cell as dirty *before* taking the grid * snapshot. */ term->render.last_cursor.row = NULL; if (term->window != NULL) { tll_foreach(term->window->urls, it) { wayl_win_subsurface_destroy(&it->item.surf); tll_remove(term->window->urls, it); /* Work around Sway bug - unmapping a sub-surface does not * damage the underlying surface */ quirk_sway_subsurface_unmap(term); } } tll_foreach(term->urls, it) { url_destroy(&it->item); tll_remove(term->urls, it); } term->urls_show_uri_on_jump_label = false; memset(term->url_keys, 0, sizeof(term->url_keys)); /* Re-enable IME, if it was enabled before we entered URL-mode */ if (term->ime_reenable_after_url_mode) { term->ime_reenable_after_url_mode = false; term_ime_enable(term); } render_refresh(term); } foot-1.21.0/url-mode.h000066400000000000000000000016131476600145200144450ustar00rootroot00000000000000#pragma once #include #include #include #include "config.h" #include "key-binding.h" #include "terminal.h" static inline bool urls_mode_is_active(const struct terminal *term) { return tll_length(term->urls) > 0; } void urls_collect( const struct terminal *term, enum url_action action, const regex_t *preg, bool osc8, url_list_t *urls); void urls_assign_key_combos(const struct config *conf, url_list_t *urls); void urls_render(struct terminal *term, const struct config_spawn_template *launch); void urls_reset(struct terminal *term); void urls_input(struct seat *seat, struct terminal *term, const struct key_binding_set *bindings, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, const xkb_keysym_t *raw_syms, size_t raw_count, uint32_t serial); foot-1.21.0/user-notification.c000066400000000000000000000005621476600145200163600ustar00rootroot00000000000000#include "user-notification.h" #include #include "xmalloc.h" void user_notification_add_fmt(user_notifications_t *notifications, enum user_notification_kind kind, const char *fmt, ...) { va_list ap; va_start(ap, fmt); char *text = xvasprintf(fmt, ap); va_end(ap); user_notification_add(notifications, kind, text); } foot-1.21.0/user-notification.h000066400000000000000000000017211476600145200163630ustar00rootroot00000000000000#pragma once #include #include "macros.h" enum user_notification_kind { USER_NOTIFICATION_DEPRECATED, USER_NOTIFICATION_WARNING, USER_NOTIFICATION_ERROR, }; struct user_notification { enum user_notification_kind kind; char *text; }; typedef tll(struct user_notification) user_notifications_t; static inline void user_notifications_free(user_notifications_t *notifications) { tll_foreach(*notifications, it) free(it->item.text); tll_free(*notifications); } static inline void user_notification_add(user_notifications_t *notifications, enum user_notification_kind kind, char *text) { struct user_notification notification = { .kind = kind, .text = text }; tll_push_back(*notifications, notification); } void user_notification_add_fmt(user_notifications_t *notifications, enum user_notification_kind kind, const char *fmt, ...) PRINTF(3); foot-1.21.0/util.h000066400000000000000000000024751476600145200137050ustar00rootroot00000000000000#pragma once #include #include #include #include #define ALEN(v) (sizeof(v) / sizeof((v)[0])) #define min(x, y) ((x) < (y) ? (x) : (y)) #define max(x, y) ((x) > (y) ? (x) : (y)) static inline bool streq(const char *a, const char *b) { return strcmp(a, b) == 0; } static inline const char * thrd_err_as_string(int thrd_err) { switch (thrd_err) { case thrd_success: return "success"; case thrd_busy: return "busy"; case thrd_nomem: return "no memory"; case thrd_timedout: return "timedout"; case thrd_error: default: return "unknown error"; } return "unknown error"; } static inline uint64_t sdbm_hash(const char *s) { uint64_t hash = 0; for (; *s != '\0'; s++) { int c = *s; hash = c + (hash << 6) + (hash << 16) - hash; } return hash; } enum { HEX_DIGIT_INVALID = 16 }; static inline uint8_t hex2nibble(char c) { switch (c) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': return c - '0'; case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': return c - 'a' + 10; case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': return c - 'A' + 10; } return HEX_DIGIT_INVALID; } foot-1.21.0/utils/000077500000000000000000000000001476600145200137075ustar00rootroot00000000000000foot-1.21.0/utils/meson.build000066400000000000000000000000471476600145200160520ustar00rootroot00000000000000executable('xtgettcap', 'xtgettcap.c') foot-1.21.0/utils/xtgettcap.c000066400000000000000000000120561476600145200160620ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include static struct termios orig_termios; static void disable_raw_mode(void) { if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) < 0) exit(__LINE__); } static void enable_raw_mode(void) { if (tcgetattr(STDIN_FILENO, &orig_termios) < 0) exit(__LINE__); atexit(disable_raw_mode); struct termios raw = orig_termios; raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); raw.c_oflag &= ~(OPOST); raw.c_cflag |= (CS8); raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); raw.c_cc[VMIN] = 0; raw.c_cc[VTIME] = 1; if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) < 0) exit(__LINE__); } static const char * hexlify(const char *s) { static char buf[1024]; const size_t len = strlen(s); for (size_t i = 0; i < len; i++) sprintf(&buf[i * 2], "%02x", s[i]); buf[len * 2 + 1] = '\0'; return buf; } static size_t unhexlify(char *dst, const char *src) { size_t count = 0; for (const char *p = src; *p != '\0'; p += 2, dst++, count++) sscanf(p, "%02hhx", (unsigned char *)dst); *dst = '\0'; return count; } int main(int argc, const char *const *argv) { const size_t query_count = argc - 1; if (query_count == 0) return 0; enable_raw_mode(); printf("\x1bP+q"); for (int i = 1; i < argc; i++) printf("%s%s", i > 1 ? ";" : "", hexlify(argv[i])); printf("\033\\"); fflush(NULL); size_t replies = 0; while (replies < query_count) { struct pollfd fds[] = {{.fd = STDIN_FILENO, .events = POLLIN}}; int r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1); if (r < 0) exit(__LINE__); char buf[1024] = {0}; ssize_t count = read(STDIN_FILENO, buf, sizeof(buf)); if (count < 0) exit(__LINE__); if (count == 1 && buf[0] == 'q') break; printf("reply: (%zd chars): ", count); for (size_t i = 0; i < (size_t)count; i++) { if (isprint(buf[i])) printf("%c", buf[i]); else if (buf[i] == '\033') printf("\033[1;31m\033[m"); else printf("%02x", (uint8_t)buf[i]); } printf("\r\n"); const char *p = buf; const char *end = buf + count; while (p < end) { const char *ST = strstr(p, "\033\\"); if (ST == NULL) break; if (count < 5 || (strncmp(p, "\033P1+r", 5) != 00 && strncmp(p, "\033P0+r", 5) != 0)) { break; } const bool success = p[2] == '1'; char decoded[1024]; char copy[ST - &p[5] + 1]; strncpy(copy, &p[5], ST - &p[5]); copy[ST - &p[5]] = '\0'; char *saveptr = NULL; for (char *key_value = strtok_r(copy, "; ", &saveptr); key_value != NULL; key_value = strtok_r(NULL, "; ", &saveptr)) { // printf("key-value=%s\n", key_value); const char *key = strtok(key_value, "="); const char *value = strtok(NULL, "="); if (key == NULL) continue; #if 0 assert((success && value != NULL) || (!success && value == NULL)); #endif //printf("key=%s, value=%s\n", key, value); size_t len = unhexlify(decoded, key); if (value != NULL) { decoded[len++] = '='; len += unhexlify(&decoded[len], value); } const int color = success ? 39 : 31; printf(" \033[%dm", color); for (size_t i = 0 ; i < len; i++) { if (isprint(decoded[i])) { /* All printable characters */ printf("%c", decoded[i]); } else if (decoded[i] == '\033') { /* ESC */ printf("\033[1;31m\033[22;%dm", color); } else if (decoded[i] >= '\x00' && decoded[i] <= '\x5f') { /* Control characters, e.g. ^G etc */ printf("\033[1m^%c\033[22m", decoded[i] + '@'); } else if (decoded[i] == '\x7f') { /* Control character ^? */ printf("\033[1m^?\033[22m"); } else { /* Unknown: print hex representation */ printf("\033[1m%02x\033[22m", (uint8_t)decoded[i]); } } printf("\033[m\r\n"); replies++; } p = ST + 2; } } return 0; } foot-1.21.0/vt.c000066400000000000000000001230651476600145200133530ustar00rootroot00000000000000#include "vt.h" #include #include #include #if defined(FOOT_GRAPHEME_CLUSTERING) #include #endif #define LOG_MODULE "vt" #define LOG_ENABLE_DBG 0 #include "log.h" #include "char32.h" #include "config.h" #include "csi.h" #include "dcs.h" #include "debug.h" #include "osc.h" #include "sixel.h" #include "util.h" #include "xmalloc.h" #define UNHANDLED() LOG_DBG("unhandled: %s", esc_as_string(term, final)) /* https://vt100.net/emu/dec_ansi_parser */ enum state { STATE_GROUND, STATE_ESCAPE, STATE_ESCAPE_INTERMEDIATE, STATE_CSI_ENTRY, STATE_CSI_PARAM, STATE_CSI_INTERMEDIATE, STATE_CSI_IGNORE, STATE_OSC_STRING, STATE_DCS_ENTRY, STATE_DCS_PARAM, STATE_DCS_INTERMEDIATE, STATE_DCS_IGNORE, STATE_DCS_PASSTHROUGH, STATE_SOS_PM_APC_STRING, STATE_UTF8_21, STATE_UTF8_31, STATE_UTF8_32, STATE_UTF8_41, STATE_UTF8_42, STATE_UTF8_43, }; #if defined(_DEBUG) && defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG && 0 static const char *const state_names[] = { [STATE_GROUND] = "ground", [STATE_ESCAPE] = "escape", [STATE_ESCAPE_INTERMEDIATE] = "escape intermediate", [STATE_CSI_ENTRY] = "CSI entry", [STATE_CSI_PARAM] = "CSI param", [STATE_CSI_INTERMEDIATE] = "CSI intermediate", [STATE_CSI_IGNORE] = "CSI ignore", [STATE_OSC_STRING] = "OSC string", [STATE_DCS_ENTRY] = "DCS entry", [STATE_DCS_PARAM] = "DCS param", [STATE_DCS_INTERMEDIATE] = "DCS intermediate", [STATE_DCS_IGNORE] = "DCS ignore", [STATE_DCS_PASSTHROUGH] = "DCS passthrough", [STATE_SOS_PM_APC_STRING] = "sos/pm/apc string", [STATE_UTF8_21] = "UTF8 2-byte 1/2", [STATE_UTF8_31] = "UTF8 3-byte 1/3", [STATE_UTF8_32] = "UTF8 3-byte 2/3", }; #endif #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG static const char * esc_as_string(struct terminal *term, uint8_t final) { static char msg[1024]; int c = snprintf(msg, sizeof(msg), "\\E"); for (size_t i = 0; i < sizeof(term->vt.private); i++) { char value = (term->vt.private >> (i * 8)) & 0xff; if (value == 0) break; c += snprintf(&msg[c], sizeof(msg) - c, "%c", value); } xassert(term->vt.params.idx == 0); snprintf(&msg[c], sizeof(msg) - c, "%c", final); return msg; } #endif static void action_ignore(struct terminal *term) { } static void action_clear(struct terminal *term) { term->vt.params.idx = 0; term->vt.private = 0; } static void action_execute(struct terminal *term, uint8_t c) { LOG_DBG("execute: 0x%02x", c); switch (c) { /* * 7-bit C0 control characters */ case '\0': break; case '\a': /* BEL - bell */ term_bell(term); break; case '\b': /* backspace */ #if 0 /* * This is the "correct" BS behavior. However, it doesn't play * nicely with bw/auto_left_margin, hence the alternative * implementation below. * * Note that it breaks vttest "1. Test of cursor movements -> * Test of autowrap" */ term_cursor_left(term, 1); #else if (term->grid->cursor.lcf) term->grid->cursor.lcf = false; else { /* Reverse wrap */ if (unlikely(term->grid->cursor.point.col == 0) && likely(term->reverse_wrap && term->auto_margin)) { if (term->grid->cursor.point.row <= term->scroll_region.start) { /* Don't wrap past, or inside, the scrolling region(?) */ } else term_cursor_to( term, term->grid->cursor.point.row - 1, term->cols - 1); } else term_cursor_left(term, 1); } #endif break; case '\t': { /* HT - horizontal tab */ int start_col = term->grid->cursor.point.col; int new_col = term->cols - 1; tll_foreach(term->tab_stops, it) { if (it->item > start_col) { new_col = it->item; break; } } xassert(new_col >= start_col); xassert(new_col < term->cols); struct row *row = term->grid->cur_row; bool emit_tab_char = (row->cells[start_col].wc == 0 || row->cells[start_col].wc == U' '); /* Check if all cells from here until the next tab stop are empty */ for (const struct cell *cell = &row->cells[start_col + 1]; cell < &row->cells[new_col]; cell++) { if (!(cell->wc == 0 || cell->wc == U' ')) { emit_tab_char = false; break; } } /* * Emit a tab in current cell, and write spaces to the * subsequent cells, all the way until the next tab stop. */ if (emit_tab_char) { row->dirty = true; row->cells[start_col].wc = U'\t'; row->cells[start_col].attrs.clean = 0; for (struct cell *cell = &row->cells[start_col + 1]; cell < &row->cells[new_col]; cell++) { cell->wc = U' '; cell->attrs.clean = 0; } } /* According to the specification, HT _should_ cancel LCF. But * XTerm, and nearly all other emulators, don't. So we follow * suit */ bool lcf = term->grid->cursor.lcf; term_cursor_right(term, new_col - start_col); term->grid->cursor.lcf = lcf; break; } case '\n': case '\v': case '\f': /* LF - \n - line feed */ /* VT - \v - vertical tab */ /* FF - \f - form feed */ term_linefeed(term); break; case '\r': /* CR - carriage ret */ term_carriage_return(term); break; case '\x0e': /* SO - shift out */ term->charsets.selected = G1; term->bits_affecting_ascii_printer.charset = term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); break; case '\x0f': /* SI - shift in */ term->charsets.selected = G0; term->bits_affecting_ascii_printer.charset = term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); break; /* * 8-bit C1 control characters * * We ignore these, but keep them here for reference, along * with their corresponding 7-bit variants. * * As far as I can tell, XTerm also ignores these _when in * UTF-8 mode_. Which would be the normal mode of operation * these days. And since we _only_ support UTF-8... */ #if 0 case '\x84': /* IND -> ESC D */ case '\x85': /* NEL -> ESC E */ case '\x88': /* Tab Set -> ESC H */ case '\x8d': /* RI -> ESC M */ case '\x8e': /* SS2 -> ESC N */ case '\x8f': /* SS3 -> ESC O */ case '\x90': /* DCS -> ESC P */ case '\x96': /* SPA -> ESC V */ case '\x97': /* EPA -> ESC W */ case '\x98': /* SOS -> ESC X */ case '\x9a': /* DECID -> ESC Z (obsolete form of CSI c) */ case '\x9b': /* CSI -> ESC [ */ case '\x9c': /* ST -> ESC \ */ case '\x9d': /* OSC -> ESC ] */ case '\x9e': /* PM -> ESC ^ */ case '\x9f': /* APC -> ESC _ */ break; #endif default: break; } } static void action_print(struct terminal *term, uint8_t c) { term_reset_grapheme_state(term); term->ascii_printer(term, c); } static void action_param_lazy_init(struct terminal *term) { if (term->vt.params.idx == 0) { struct vt_param *param = &term->vt.params.v[0]; term->vt.params.cur = param; param->value = 0; param->sub.idx = 0; param->sub.cur = NULL; term->vt.params.idx = 1; } } static void action_param_new(struct terminal *term, uint8_t c) { xassert(c == ';'); action_param_lazy_init(term); const size_t max_params = sizeof(term->vt.params.v) / sizeof(term->vt.params.v[0]); struct vt_param *param; if (unlikely(term->vt.params.idx >= max_params)) { static bool have_warned = false; if (!have_warned) { have_warned = true; LOG_WARN( "unsupported: escape with more than %zu parameters " "(will not warn again)", sizeof(term->vt.params.v) / sizeof(term->vt.params.v[0])); } param = &term->vt.params.dummy; } else param = &term->vt.params.v[term->vt.params.idx++]; term->vt.params.cur = param; param->value = 0; param->sub.idx = 0; param->sub.cur = NULL; } static void action_param_new_subparam(struct terminal *term, uint8_t c) { xassert(c == ':'); action_param_lazy_init(term); const size_t max_sub_params = sizeof(term->vt.params.v[0].sub.value) / sizeof(term->vt.params.v[0].sub.value[0]); struct vt_param *param = term->vt.params.cur; unsigned *sub_param_value; if (unlikely(param->sub.idx >= max_sub_params)) { static bool have_warned = false; if (!have_warned) { have_warned = true; LOG_WARN( "unsupported: escape with more than %zu sub-parameters " "(will not warn again)", sizeof(term->vt.params.v[0].sub.value) / sizeof(term->vt.params.v[0].sub.value[0])); } sub_param_value = ¶m->sub.dummy; } else sub_param_value = ¶m->sub.value[param->sub.idx++]; param->sub.cur = sub_param_value; *sub_param_value = 0; } static void action_param(struct terminal *term, uint8_t c) { action_param_lazy_init(term); xassert(term->vt.params.cur != NULL); struct vt_param *param = term->vt.params.cur; unsigned *value; if (unlikely(param->sub.cur != NULL)) value = param->sub.cur; else value = ¶m->value; unsigned v = *value; v *= 10; v += c - '0'; *value = v; } static void action_collect(struct terminal *term, uint8_t c) { LOG_DBG("collect: %c", c); /* * Having more than one private is *very* rare. Foot only supports * a *single* escape with two privates, and none with three or * more. * * As such, we optimize *reading* the private(s), and *resetting* * them (in action_clear()). Writing is ok if it's a bit slow. */ if ((term->vt.private & 0xff) == 0) term->vt.private = c; else if (((term->vt.private >> 8) & 0xff) == 0) term->vt.private |= c << 8; else if (((term->vt.private >> 16) & 0xff) == 0) term->vt.private |= c << 16; else if (((term->vt.private >> 24) & 0xff) == 0) term->vt.private |= c << 24; else LOG_WARN("only four private/intermediate characters supported"); } UNITTEST { struct terminal term = {.vt = {.private = 0}}; uint32_t expected = ' '; action_collect(&term, ' '); xassert(term.vt.private == expected); expected |= '/' << 8; action_collect(&term, '/'); xassert(term.vt.private == expected); expected |= '<' << 16; action_collect(&term, '<'); xassert(term.vt.private == expected); expected |= '?' << 24; action_collect(&term, '?'); xassert(term.vt.private == expected); action_collect(&term, '?'); xassert(term.vt.private == expected); } static void tab_set(struct terminal *term) { int col = term->grid->cursor.point.col; if (tll_length(term->tab_stops) == 0 || tll_back(term->tab_stops) < col) { tll_push_back(term->tab_stops, col); return; } tll_foreach(term->tab_stops, it) { if (it->item < col) { continue; } if (it->item > col) { tll_insert_before(term->tab_stops, it, col); } break; } } static void action_esc_dispatch(struct terminal *term, uint8_t final) { LOG_DBG("ESC: %s", esc_as_string(term, final)); switch (term->vt.private) { case 0: switch (final) { case '7': term_save_cursor(term); break; case '8': term_restore_cursor(term, &term->grid->saved_cursor); break; case 'c': term_reset(term, true); break; case 'n': /* LS2 - Locking Shift 2 */ term->charsets.selected = G2; term->bits_affecting_ascii_printer.charset = term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); break; case 'o': /* LS3 - Locking Shift 3 */ term->charsets.selected = G3; term->bits_affecting_ascii_printer.charset = term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); break; case 'D': term_linefeed(term); break; case 'E': term_carriage_return(term); term_linefeed(term); break; case 'H': tab_set(term); break; case 'M': term_reverse_index(term); break; case 'N': /* SS2 - Single Shift 2 */ term_single_shift(term, G2); break; case 'O': /* SS3 - Single Shift 3 */ term_single_shift(term, G3); break; case '\\': /* ST - String Terminator */ break; case '=': term->keypad_keys_mode = KEYPAD_APPLICATION; break; case '>': term->keypad_keys_mode = KEYPAD_NUMERICAL; break; default: UNHANDLED(); break; } break; /* private[0] == 0 */ // Designate character set case '(': // G0 case ')': // G1 case '*': // G2 case '+': // G3 switch (final) { case '0': { size_t idx = term->vt.private - '('; xassert(idx <= G3); term->charsets.set[idx] = CHARSET_GRAPHIC; term->bits_affecting_ascii_printer.charset = term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); break; } case 'B': { size_t idx = term->vt.private - '('; xassert(idx <= G3); term->charsets.set[idx] = CHARSET_ASCII; term->bits_affecting_ascii_printer.charset = term->charsets.set[term->charsets.selected] != CHARSET_ASCII; term_update_ascii_printer(term); break; } } break; case '#': switch (final) { case '8': /* DECALN */ sixel_overwrite_by_rectangle(term, 0, 0, term->rows, term->cols); term->scroll_region.start = 0; term->scroll_region.end = term->rows; for (int r = 0; r < term->rows; r++) term_fill(term, r, 0, 'E', term->cols, false); term_cursor_home(term); break; } break; /* private[0] == '#' */ } } static void action_csi_dispatch(struct terminal *term, uint8_t c) { csi_dispatch(term, c); } static void action_osc_start(struct terminal *term, uint8_t c) { term->vt.osc.idx = 0; } static void action_osc_end(struct terminal *term, uint8_t c) { struct vt *vt = &term->vt; if (!osc_ensure_size(term, vt->osc.idx + 1)) return; vt->osc.data[vt->osc.idx] = '\0'; vt->osc.bel = c == '\a'; osc_dispatch(term); if (unlikely(vt->osc.idx >= 4096)) { free(vt->osc.data); vt->osc.data = NULL; vt->osc.size = 0; } } static void action_osc_put(struct terminal *term, uint8_t c) { if (!osc_ensure_size(term, term->vt.osc.idx + 1)) return; term->vt.osc.data[term->vt.osc.idx++] = c; } static void action_hook(struct terminal *term, uint8_t c) { dcs_hook(term, c); } static void action_unhook(struct terminal *term, uint8_t c) { dcs_unhook(term); } static void action_put(struct terminal *term, uint8_t c) { dcs_put(term, c); } static void action_utf8_print(struct terminal *term, char32_t wc) { term_process_and_print_non_ascii(term, wc); } static void action_utf8_21(struct terminal *term, uint8_t c) { // wc = ((utf8[0] & 0x1f) << 6) | (utf8[1] & 0x3f) term->vt.utf8 = (c & 0x1f) << 6; } static void action_utf8_22(struct terminal *term, uint8_t c) { // wc = ((utf8[0] & 0x1f) << 6) | (utf8[1] & 0x3f) term->vt.utf8 |= c & 0x3f; action_utf8_print(term, term->vt.utf8); } static void action_utf8_31(struct terminal *term, uint8_t c) { // wc = ((utf8[0] & 0xf) << 12) | ((utf8[1] & 0x3f) << 6) | (utf8[2] & 0x3f) term->vt.utf8 = (c & 0x0f) << 12; } static void action_utf8_32(struct terminal *term, uint8_t c) { // wc = ((utf8[0] & 0xf) << 12) | ((utf8[1] & 0x3f) << 6) | (utf8[2] & 0x3f) term->vt.utf8 |= (c & 0x3f) << 6; } static void action_utf8_33(struct terminal *term, uint8_t c) { // wc = ((utf8[0] & 0xf) << 12) | ((utf8[1] & 0x3f) << 6) | (utf8[2] & 0x3f) term->vt.utf8 |= c & 0x3f; const char32_t utf32 = term->vt.utf8; if (unlikely(utf32 >= 0xd800 && utf32 <= 0xdfff)) { /* Invalid sequence - invalid UTF-16 surrogate halves */ return; } /* Note: the E0 range contains overlong encodings. We don't try to detect, as they'll still decode to valid UTF-32. */ action_utf8_print(term, term->vt.utf8); } static void action_utf8_41(struct terminal *term, uint8_t c) { // wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f); term->vt.utf8 = (c & 0x07) << 18; } static void action_utf8_42(struct terminal *term, uint8_t c) { // wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f); term->vt.utf8 |= (c & 0x3f) << 12; } static void action_utf8_43(struct terminal *term, uint8_t c) { // wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f); term->vt.utf8 |= (c & 0x3f) << 6; } static void action_utf8_44(struct terminal *term, uint8_t c) { // wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f); term->vt.utf8 |= c & 0x3f; const char32_t utf32 = term->vt.utf8; if (unlikely(utf32 > 0x10FFFF)) { /* Invalid UTF-8 */ return; } /* Note: the F0 range contains overlong encodings. We don't try to detect, as they'll still decode to valid UTF-32. */ action_utf8_print(term, term->vt.utf8); } IGNORE_WARNING("-Wpedantic") static enum state anywhere(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x18: action_execute(term, data); return STATE_GROUND; case 0x1a: action_execute(term, data); return STATE_GROUND; case 0x1b: action_clear(term); return STATE_ESCAPE; /* 8-bit C1 control characters (not supported) */ case 0x80 ... 0x9f: return STATE_GROUND; } return term->vt.state; } static enum state state_ground_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x00 ... 0x17: case 0x19: case 0x1c ... 0x1f: action_execute(term, data); return STATE_GROUND; /* modified from 0x20..0x7f to 0x20..0x7e, since 0x7f is DEL, which is a zero-width character */ case 0x20 ... 0x7e: action_print(term, data); return STATE_GROUND; case 0xc2 ... 0xdf: action_utf8_21(term, data); return STATE_UTF8_21; case 0xe0 ... 0xef: action_utf8_31(term, data); return STATE_UTF8_31; case 0xf0 ... 0xf4: action_utf8_41(term, data); return STATE_UTF8_41; } return anywhere(term, data); } static enum state state_escape_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x00 ... 0x17: case 0x19: case 0x1c ... 0x1f: action_execute(term, data); return STATE_ESCAPE; case 0x20 ... 0x2f: action_collect(term, data); return STATE_ESCAPE_INTERMEDIATE; case 0x30 ... 0x4f: action_esc_dispatch(term, data); return STATE_GROUND; case 0x50: action_clear(term); return STATE_DCS_ENTRY; case 0x51 ... 0x57: action_esc_dispatch(term, data); return STATE_GROUND; case 0x58: return STATE_SOS_PM_APC_STRING; case 0x59: action_esc_dispatch(term, data); return STATE_GROUND; case 0x5a: action_esc_dispatch(term, data); return STATE_GROUND; case 0x5b: action_clear(term); return STATE_CSI_ENTRY; case 0x5c: action_esc_dispatch(term, data); return STATE_GROUND; case 0x5d: action_osc_start(term, data); return STATE_OSC_STRING; case 0x5e ... 0x5f: return STATE_SOS_PM_APC_STRING; case 0x60 ... 0x7e: action_esc_dispatch(term, data); return STATE_GROUND; case 0x7f: action_ignore(term); return STATE_ESCAPE; } return anywhere(term, data); } static enum state state_escape_intermediate_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x00 ... 0x17: case 0x19: case 0x1c ... 0x1f: action_execute(term, data); return STATE_ESCAPE_INTERMEDIATE; case 0x20 ... 0x2f: action_collect(term, data); return STATE_ESCAPE_INTERMEDIATE; case 0x30 ... 0x7e: action_esc_dispatch(term, data); return STATE_GROUND; case 0x7f: action_ignore(term); return STATE_ESCAPE_INTERMEDIATE; } return anywhere(term, data); } static enum state state_csi_entry_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x00 ... 0x17: case 0x19: case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_ENTRY; case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE; case 0x30 ... 0x39: action_param(term, data); return STATE_CSI_PARAM; case 0x3a: action_param_new_subparam(term, data); return STATE_CSI_PARAM; case 0x3b: action_param_new(term, data); return STATE_CSI_PARAM; case 0x3c ... 0x3f: action_collect(term, data); return STATE_CSI_PARAM; case 0x40 ... 0x7e: action_csi_dispatch(term, data); return STATE_GROUND; case 0x7f: action_ignore(term); return STATE_CSI_ENTRY; } return anywhere(term, data); } static enum state state_csi_param_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x00 ... 0x17: case 0x19: case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_PARAM; case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE; case 0x30 ... 0x39: action_param(term, data); return STATE_CSI_PARAM; case 0x3a: action_param_new_subparam(term, data); return STATE_CSI_PARAM; case 0x3b: action_param_new(term, data); return STATE_CSI_PARAM; case 0x3c ... 0x3f: return STATE_CSI_IGNORE; case 0x40 ... 0x7e: action_csi_dispatch(term, data); return STATE_GROUND; case 0x7f: action_ignore(term); return STATE_CSI_PARAM; } return anywhere(term, data); } static enum state state_csi_intermediate_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x00 ... 0x17: case 0x19: case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_INTERMEDIATE; case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE; case 0x30 ... 0x3f: return STATE_CSI_IGNORE; case 0x40 ... 0x7e: action_csi_dispatch(term, data); return STATE_GROUND; case 0x7f: action_ignore(term); return STATE_CSI_INTERMEDIATE; } return anywhere(term, data); } static enum state state_csi_ignore_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x00 ... 0x17: case 0x19: case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_IGNORE; case 0x20 ... 0x3f: action_ignore(term); return STATE_CSI_IGNORE; case 0x40 ... 0x7e: return STATE_GROUND; case 0x7f: action_ignore(term); return STATE_CSI_IGNORE; } return anywhere(term, data); } static enum state state_osc_string_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ /* Note: original was 20-7f, but I changed to 20-ff to include utf-8. Don't forget to add EXECUTE to 8-bit C1 if we implement that. */ default: action_osc_put(term, data); return STATE_OSC_STRING; case 0x07: action_osc_end(term, data); return STATE_GROUND; case 0x00 ... 0x06: case 0x08 ... 0x17: case 0x19: case 0x1c ... 0x1f: action_ignore(term); return STATE_OSC_STRING; case 0x18: case 0x1a: action_osc_end(term, data); action_execute(term, data); return STATE_GROUND; case 0x1b: action_osc_end(term, data); action_clear(term); return STATE_ESCAPE; } } static enum state state_dcs_entry_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x00 ... 0x17: case 0x19: case 0x1c ... 0x1f: action_ignore(term); return STATE_DCS_ENTRY; case 0x20 ... 0x2f: action_collect(term, data); return STATE_DCS_INTERMEDIATE; case 0x30 ... 0x39: action_param(term, data); return STATE_DCS_PARAM; case 0x3a: return STATE_DCS_IGNORE; case 0x3b: action_param_new(term, data); return STATE_DCS_PARAM; case 0x3c ... 0x3f: action_collect(term, data); return STATE_DCS_PARAM; case 0x40 ... 0x7e: action_hook(term, data); return STATE_DCS_PASSTHROUGH; case 0x7f: action_ignore(term); return STATE_DCS_ENTRY; } return anywhere(term, data); } static enum state state_dcs_param_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x00 ... 0x17: case 0x19: case 0x1c ... 0x1f: action_ignore(term); return STATE_DCS_PARAM; case 0x20 ... 0x2f: action_collect(term, data); return STATE_DCS_INTERMEDIATE; case 0x30 ... 0x39: action_param(term, data); return STATE_DCS_PARAM; case 0x3a: return STATE_DCS_IGNORE; case 0x3b: action_param_new(term, data); return STATE_DCS_PARAM; case 0x3c ... 0x3f: return STATE_DCS_IGNORE; case 0x40 ... 0x7e: action_hook(term, data); return STATE_DCS_PASSTHROUGH; case 0x7f: action_ignore(term); return STATE_DCS_PARAM; } return anywhere(term, data); } static enum state state_dcs_intermediate_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x00 ... 0x17: case 0x19: case 0x1c ... 0x1f: action_ignore(term); return STATE_DCS_INTERMEDIATE; case 0x20 ... 0x2f: action_collect(term, data); return STATE_DCS_INTERMEDIATE; case 0x30 ... 0x3f: return STATE_DCS_IGNORE; case 0x40 ... 0x7e: action_hook(term, data); return STATE_DCS_PASSTHROUGH; case 0x7f: action_ignore(term); return STATE_DCS_INTERMEDIATE; } return anywhere(term, data); } static enum state state_dcs_ignore_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x00 ... 0x17: case 0x19: case 0x1c ... 0x1f: case 0x20 ... 0x7f: action_ignore(term); return STATE_DCS_IGNORE; } return anywhere(term, data); } static enum state state_dcs_passthrough_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x00 ... 0x17: case 0x19: case 0x1c ... 0x7e: action_put(term, data); return STATE_DCS_PASSTHROUGH; case 0x7f: action_ignore(term); return STATE_DCS_PASSTHROUGH; /* Anywhere */ case 0x18: action_unhook(term, data); action_execute(term, data); return STATE_GROUND; case 0x1a: action_unhook(term, data); action_execute(term, data); return STATE_GROUND; case 0x1b: action_unhook(term, data); action_clear(term); return STATE_ESCAPE; /* 8-bit C1 control characters (not supported) */ case 0x80 ... 0x9f: action_unhook(term, data); return STATE_GROUND; default: return STATE_DCS_PASSTHROUGH; } } static enum state state_sos_pm_apc_string_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x00 ... 0x17: case 0x19: case 0x1c ... 0x7f: action_ignore(term); return STATE_SOS_PM_APC_STRING; } return anywhere(term, data); } static enum state state_utf8_21_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_22(term, data); return STATE_GROUND; default: return STATE_GROUND; } } static enum state state_utf8_31_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_32(term, data); return STATE_UTF8_32; default: return STATE_GROUND; } } static enum state state_utf8_32_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_33(term, data); return STATE_GROUND; default: return STATE_GROUND; } } static enum state state_utf8_41_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_42(term, data); return STATE_UTF8_42; default: return STATE_GROUND; } } static enum state state_utf8_42_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_43(term, data); return STATE_UTF8_43; default: return STATE_GROUND; } } static enum state state_utf8_43_switch(struct terminal *term, uint8_t data) { switch (data) { /* exit current enter new state */ case 0x80 ... 0xbf: action_utf8_44(term, data); return STATE_GROUND; default: return STATE_GROUND; } } UNIGNORE_WARNINGS void vt_from_slave(struct terminal *term, const uint8_t *data, size_t len) { enum state current_state = term->vt.state; const uint8_t *p = data; for (size_t i = 0; i < len; i++, p++) { switch (current_state) { case STATE_GROUND: current_state = state_ground_switch(term, *p); break; case STATE_ESCAPE: current_state = state_escape_switch(term, *p); break; case STATE_ESCAPE_INTERMEDIATE: current_state = state_escape_intermediate_switch(term, *p); break; case STATE_CSI_ENTRY: current_state = state_csi_entry_switch(term, *p); break; case STATE_CSI_PARAM: current_state = state_csi_param_switch(term, *p); break; case STATE_CSI_INTERMEDIATE: current_state = state_csi_intermediate_switch(term, *p); break; case STATE_CSI_IGNORE: current_state = state_csi_ignore_switch(term, *p); break; case STATE_OSC_STRING: current_state = state_osc_string_switch(term, *p); break; case STATE_DCS_ENTRY: current_state = state_dcs_entry_switch(term, *p); break; case STATE_DCS_PARAM: current_state = state_dcs_param_switch(term, *p); break; case STATE_DCS_INTERMEDIATE: current_state = state_dcs_intermediate_switch(term, *p); break; case STATE_DCS_IGNORE: current_state = state_dcs_ignore_switch(term, *p); break; case STATE_DCS_PASSTHROUGH: current_state = state_dcs_passthrough_switch(term, *p); break; case STATE_SOS_PM_APC_STRING: current_state = state_sos_pm_apc_string_switch(term, *p); break; case STATE_UTF8_21: current_state = state_utf8_21_switch(term, *p); break; case STATE_UTF8_31: current_state = state_utf8_31_switch(term, *p); break; case STATE_UTF8_32: current_state = state_utf8_32_switch(term, *p); break; case STATE_UTF8_41: current_state = state_utf8_41_switch(term, *p); break; case STATE_UTF8_42: current_state = state_utf8_42_switch(term, *p); break; case STATE_UTF8_43: current_state = state_utf8_43_switch(term, *p); break; } term->vt.state = current_state; } } foot-1.21.0/vt.h000066400000000000000000000014411476600145200133510ustar00rootroot00000000000000#pragma once #include #include #include #include "terminal.h" void vt_from_slave(struct terminal *term, const uint8_t *data, size_t len); static inline int vt_param_get(const struct terminal *term, size_t idx, int default_value) { /* * We zero excess bits in parsed param values. In most cases this will * effectively be a no-op; but it prevents negative returns for edge * cases involving unusually large values. */ static_assert(INT_MAX >= 0x7fffffff, "POSIX requires INT_MAX >= 0x7fffffff"); const unsigned value_mask = 0x7fffffff; if (term->vt.params.idx > idx) { unsigned value = term->vt.params.v[idx].value & value_mask; return value != 0 ? (int)value : default_value; } return default_value; } foot-1.21.0/wayland.c000066400000000000000000002411321476600145200143550ustar00rootroot00000000000000#include "wayland.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define LOG_MODULE "wayland" #define LOG_ENABLE_DBG 0 #include "log.h" #include "config.h" #include "terminal.h" #include "ime.h" #include "input.h" #include "render.h" #include "selection.h" #include "shm.h" #include "shm-formats.h" #include "util.h" #include "xmalloc.h" static void csd_reload_font(struct wl_window *win, float old_scale) { struct terminal *term = win->term; const struct config *conf = term->conf; const float scale = term->scale; bool enable_csd = win->csd_mode == CSD_YES && !win->is_fullscreen; if (!enable_csd) return; if (win->csd.font != NULL && scale == old_scale) return; fcft_destroy(win->csd.font); const char *patterns[conf->csd.font.count]; for (size_t i = 0; i < conf->csd.font.count; i++) patterns[i] = conf->csd.font.arr[i].pattern; char pixelsize[32]; snprintf(pixelsize, sizeof(pixelsize), "pixelsize=%u", (int)roundf(conf->csd.title_height * scale * 1 / 2)); LOG_DBG("loading CSD font \"%s:%s\" (old-scale=%.2f, scale=%.2f)", patterns[0], pixelsize, old_scale, scale); win->csd.font = fcft_from_name(conf->csd.font.count, patterns, pixelsize); } static void csd_instantiate(struct wl_window *win) { struct wayland *wayl = win->term->wl; xassert(wayl != NULL); for (size_t i = 0; i < CSD_SURF_MINIMIZE; i++) { bool ret = wayl_win_subsurface_new(win, &win->csd.surface[i], true); xassert(ret); } for (size_t i = CSD_SURF_MINIMIZE; i < CSD_SURF_COUNT; i++) { bool ret = wayl_win_subsurface_new_with_custom_parent( win, win->csd.surface[CSD_SURF_TITLE].surface.surf, &win->csd.surface[i], true); xassert(ret); } csd_reload_font(win, -1.); } static void csd_destroy(struct wl_window *win) { struct terminal *term = win->term; fcft_destroy(term->window->csd.font); term->window->csd.font = NULL; for (size_t i = 0; i < ALEN(win->csd.surface); i++) wayl_win_subsurface_destroy(&win->csd.surface[i]); shm_purge(term->render.chains.csd); } static void seat_add_data_device(struct seat *seat) { if (seat->wayl->data_device_manager == NULL) return; if (seat->data_device != NULL) { /* TODO: destroy old device + clipboard data? */ return; } struct wl_data_device *data_device = wl_data_device_manager_get_data_device( seat->wayl->data_device_manager, seat->wl_seat); if (data_device == NULL) return; seat->data_device = data_device; wl_data_device_add_listener(data_device, &data_device_listener, seat); } static void seat_add_primary_selection(struct seat *seat) { if (seat->wayl->primary_selection_device_manager == NULL) return; if (seat->primary_selection_device != NULL) return; struct zwp_primary_selection_device_v1 *primary_selection_device = zwp_primary_selection_device_manager_v1_get_device( seat->wayl->primary_selection_device_manager, seat->wl_seat); if (primary_selection_device == NULL) return; seat->primary_selection_device = primary_selection_device; zwp_primary_selection_device_v1_add_listener( primary_selection_device, &primary_selection_device_listener, seat); } static void seat_add_text_input(struct seat *seat) { #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (seat->wayl->text_input_manager == NULL) return; struct zwp_text_input_v3 *text_input = zwp_text_input_manager_v3_get_text_input( seat->wayl->text_input_manager, seat->wl_seat); if (text_input == NULL) return; seat->wl_text_input = text_input; zwp_text_input_v3_add_listener(text_input, &text_input_listener, seat); #endif } static void seat_add_key_bindings(struct seat *seat) { key_binding_new_for_seat(seat->wayl->key_binding_manager, seat); } static void seat_destroy(struct seat *seat) { if (seat == NULL) return; tll_free(seat->mouse.buttons); key_binding_remove_seat(seat->wayl->key_binding_manager, seat); if (seat->kbd.xkb_compose_state != NULL) xkb_compose_state_unref(seat->kbd.xkb_compose_state); if (seat->kbd.xkb_compose_table != NULL) xkb_compose_table_unref(seat->kbd.xkb_compose_table); if (seat->kbd.xkb_keymap != NULL) xkb_keymap_unref(seat->kbd.xkb_keymap); if (seat->kbd.xkb_state != NULL) xkb_state_unref(seat->kbd.xkb_state); if (seat->kbd.xkb != NULL) xkb_context_unref(seat->kbd.xkb); if (seat->kbd.repeat.fd >= 0) fdm_del(seat->wayl->fdm, seat->kbd.repeat.fd); if (seat->pointer.theme != NULL) wl_cursor_theme_destroy(seat->pointer.theme); if (seat->pointer.surface.surf != NULL) wl_surface_destroy(seat->pointer.surface.surf); if (seat->pointer.surface.viewport != NULL) wp_viewport_destroy(seat->pointer.surface.viewport); if (seat->pointer.xcursor_callback != NULL) wl_callback_destroy(seat->pointer.xcursor_callback); if (seat->clipboard.data_source != NULL) wl_data_source_destroy(seat->clipboard.data_source); if (seat->clipboard.data_offer != NULL) wl_data_offer_destroy(seat->clipboard.data_offer); if (seat->primary.data_source != NULL) zwp_primary_selection_source_v1_destroy(seat->primary.data_source); if (seat->primary.data_offer != NULL) zwp_primary_selection_offer_v1_destroy(seat->primary.data_offer); if (seat->primary_selection_device != NULL) zwp_primary_selection_device_v1_destroy(seat->primary_selection_device); if (seat->data_device != NULL) wl_data_device_release(seat->data_device); if (seat->pointer.shape_device != NULL) wp_cursor_shape_device_v1_destroy(seat->pointer.shape_device); if (seat->wl_keyboard != NULL) wl_keyboard_release(seat->wl_keyboard); if (seat->wl_pointer != NULL) wl_pointer_release(seat->wl_pointer); if (seat->wl_touch != NULL) wl_touch_release(seat->wl_touch); #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (seat->wl_text_input != NULL) zwp_text_input_v3_destroy(seat->wl_text_input); #endif if (seat->wl_seat != NULL) wl_seat_release(seat->wl_seat); ime_reset_pending(seat); free(seat->clipboard.text); free(seat->primary.text); free(seat->pointer.last_custom_xcursor); free(seat->name); } static void shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) { struct wayland *wayl = data; switch (format) { case WL_SHM_FORMAT_XRGB2101010: wayl->shm_have_xrgb2101010 = true; break; case WL_SHM_FORMAT_ARGB2101010: wayl->shm_have_argb2101010 = true; break; case WL_SHM_FORMAT_XBGR2101010: wayl->shm_have_xbgr2101010 = true; break; case WL_SHM_FORMAT_ABGR2101010: wayl->shm_have_abgr2101010 = true; break; } #if defined(_DEBUG) bool have_description = false; for (size_t i = 0; i < ALEN(shm_formats); i++) { if (shm_formats[i].format == format) { LOG_DBG("shm: 0x%08x: %s", format, shm_formats[i].description); have_description = true; break; } } if (!have_description) LOG_DBG("shm: 0x%08x: unknown", format); #endif } static const struct wl_shm_listener shm_listener = { .format = &shm_format, }; static void xdg_wm_base_ping(void *data, struct xdg_wm_base *shell, uint32_t serial) { LOG_DBG("wm base ping"); xdg_wm_base_pong(shell, serial); } static const struct xdg_wm_base_listener xdg_wm_base_listener = { .ping = &xdg_wm_base_ping, }; static void seat_handle_capabilities(void *data, struct wl_seat *wl_seat, enum wl_seat_capability caps) { struct seat *seat = data; xassert(seat->wl_seat == wl_seat); LOG_DBG("%s: keyboard=%s, pointer=%s, touch=%s", seat->name, (caps & WL_SEAT_CAPABILITY_KEYBOARD) ? "yes" : "no", (caps & WL_SEAT_CAPABILITY_POINTER) ? "yes" : "no", (caps & WL_SEAT_CAPABILITY_TOUCH) ? "yes" : "no"); if (caps & WL_SEAT_CAPABILITY_KEYBOARD) { if (seat->wl_keyboard == NULL) { seat->wl_keyboard = wl_seat_get_keyboard(wl_seat); wl_keyboard_add_listener(seat->wl_keyboard, &keyboard_listener, seat); } } else { if (seat->wl_keyboard != NULL) { wl_keyboard_release(seat->wl_keyboard); seat->wl_keyboard = NULL; } } if (caps & WL_SEAT_CAPABILITY_POINTER) { if (seat->wl_pointer == NULL) { xassert(seat->pointer.surface.surf == NULL); seat->pointer.surface.surf = wl_compositor_create_surface(seat->wayl->compositor); if (seat->pointer.surface.surf == NULL) { LOG_ERR("%s: failed to create pointer surface", seat->name); return; } if (seat->wayl->viewporter != NULL) { xassert(seat->pointer.surface.viewport == NULL); seat->pointer.surface.viewport = wp_viewporter_get_viewport( seat->wayl->viewporter, seat->pointer.surface.surf); if (seat->pointer.surface.viewport == NULL) { LOG_ERR("%s: failed to create pointer viewport", seat->name); wl_surface_destroy(seat->pointer.surface.surf); seat->pointer.surface.surf = NULL; return; } } seat->wl_pointer = wl_seat_get_pointer(wl_seat); wl_pointer_add_listener(seat->wl_pointer, &pointer_listener, seat); if (seat->wayl->cursor_shape_manager != NULL) { xassert(seat->pointer.shape_device == NULL); seat->pointer.shape_device = wp_cursor_shape_manager_v1_get_pointer( seat->wayl->cursor_shape_manager, seat->wl_pointer); } } } else { if (seat->wl_pointer != NULL) { if (seat->pointer.shape_device != NULL) { wp_cursor_shape_device_v1_destroy(seat->pointer.shape_device); seat->pointer.shape_device = NULL; } wl_pointer_release(seat->wl_pointer); wl_surface_destroy(seat->pointer.surface.surf); if (seat->pointer.surface.viewport != NULL) { wp_viewport_destroy(seat->pointer.surface.viewport); seat->pointer.surface.viewport = NULL; } if (seat->pointer.theme != NULL) wl_cursor_theme_destroy(seat->pointer.theme); if (seat->wl_touch != NULL && seat->touch.state == TOUCH_STATE_INHIBITED) { seat->touch.state = TOUCH_STATE_IDLE; } seat->wl_pointer = NULL; seat->pointer.surface.surf = NULL; seat->pointer.theme = NULL; seat->pointer.cursor = NULL; } } if (caps & WL_SEAT_CAPABILITY_TOUCH) { if (seat->wl_touch == NULL) { seat->wl_touch = wl_seat_get_touch(wl_seat); wl_touch_add_listener(seat->wl_touch, &touch_listener, seat); seat->touch.state = TOUCH_STATE_IDLE; } } else { if (seat->wl_touch != NULL) { wl_touch_release(seat->wl_touch); seat->wl_touch = NULL; } seat->touch.state = TOUCH_STATE_INHIBITED; } } static void seat_handle_name(void *data, struct wl_seat *wl_seat, const char *name) { struct seat *seat = data; free(seat->name); seat->name = xstrdup(name); } static const struct wl_seat_listener seat_listener = { .capabilities = seat_handle_capabilities, .name = seat_handle_name, }; static void update_term_for_output_change(struct terminal *term) { const float old_scale = term->scale; const float logical_width = term->width / old_scale; const float logical_height = term->height / old_scale; /* Note: order matters! term_update_scale() must come first */ bool scale_updated = term_update_scale(term); bool fonts_updated = term_font_dpi_changed(term, old_scale); term_font_subpixel_changed(term); csd_reload_font(term->window, old_scale); enum resize_options resize_opts = RESIZE_KEEP_GRID; if (fonts_updated) { /* * If the fonts have been updated, the cell dimensions have * changed. This requires a "forced" resize, since the surface * buffer dimensions may not have been updated (in which case * render_resize() normally shortcuts and returns early). */ resize_opts |= RESIZE_FORCE; } else if (!scale_updated) { /* No need to resize if neither scale nor fonts have changed */ return; } else if (term->conf->dpi_aware) { /* * If fonts are sized according to DPI, it is possible for the cell * size to remain the same when display scale changes. This will not * change the surface buffer dimensions, but will change the logical * size of the window. To ensure that the compositor is made aware of * the proper logical size, force a resize rather than allowing * render_resize() to shortcut the notification if the buffer * dimensions remain the same. */ resize_opts |= RESIZE_FORCE; } render_resize( term, (int)roundf(logical_width), (int)roundf(logical_height), resize_opts); } static void update_terms_on_monitor(struct monitor *mon) { struct wayland *wayl = mon->wayl; tll_foreach(wayl->terms, it) { struct terminal *term = it->item; tll_foreach(term->window->on_outputs, it2) { if (it2->item == mon) { update_term_for_output_change(term); break; } } } } static void output_update_ppi(struct monitor *mon) { if (mon->dim.mm.width <= 0 || mon->dim.mm.height <= 0) return; double x_inches = mon->dim.mm.width * 0.03937008; double y_inches = mon->dim.mm.height * 0.03937008; const int width = mon->dim.px_real.width; const int height = mon->dim.px_real.height; mon->ppi.real.x = mon->dim.px_real.width / x_inches; mon->ppi.real.y = mon->dim.px_real.height / y_inches; /* The *logical* size is affected by the transform */ switch (mon->transform) { case WL_OUTPUT_TRANSFORM_90: case WL_OUTPUT_TRANSFORM_270: case WL_OUTPUT_TRANSFORM_FLIPPED_90: case WL_OUTPUT_TRANSFORM_FLIPPED_270: { int swap = x_inches; x_inches = y_inches; y_inches = swap; break; } case WL_OUTPUT_TRANSFORM_NORMAL: case WL_OUTPUT_TRANSFORM_180: case WL_OUTPUT_TRANSFORM_FLIPPED: case WL_OUTPUT_TRANSFORM_FLIPPED_180: break; } const int scaled_width = mon->dim.px_scaled.width; const int scaled_height = mon->dim.px_scaled.height; mon->ppi.scaled.x = scaled_width / x_inches; mon->ppi.scaled.y = scaled_height / y_inches; const double px_diag_physical = sqrt(pow(width, 2) + pow(height, 2)); mon->dpi.physical = width == 0 && height == 0 ? 96. : px_diag_physical / mon->inch; const double px_diag_scaled = sqrt(pow(scaled_width, 2) + pow(scaled_height, 2)); mon->dpi.scaled = scaled_width == 0 && scaled_height == 0 ? 96. : px_diag_scaled / mon->inch * mon->scale; if (mon->dpi.physical > 1000) { if (mon->name != NULL) { LOG_WARN("%s: DPI=%f (physical) is unreasonable, using 96 instead", mon->name, mon->dpi.physical); } mon->dpi.physical = 96; } if (mon->dpi.scaled > 1000) { if (mon->name != NULL) { LOG_WARN("%s: DPI=%f (logical) is unreasonable, using 96 instead", mon->name, mon->dpi.scaled); } mon->dpi.scaled = 96; } } static void output_geometry(void *data, struct wl_output *wl_output, int32_t x, int32_t y, int32_t physical_width, int32_t physical_height, int32_t subpixel, const char *make, const char *model, int32_t transform) { struct monitor *mon = data; free(mon->make); free(mon->model); mon->dim.mm.width = physical_width; mon->dim.mm.height = physical_height; mon->inch = sqrt(pow(mon->dim.mm.width, 2) + pow(mon->dim.mm.height, 2)) * 0.03937008; mon->make = make != NULL ? xstrdup(make) : NULL; mon->model = model != NULL ? xstrdup(model) : NULL; mon->subpixel = subpixel; mon->transform = transform; output_update_ppi(mon); } static void output_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { if ((flags & WL_OUTPUT_MODE_CURRENT) == 0) return; struct monitor *mon = data; mon->refresh = (float)refresh / 1000; mon->dim.px_real.width = width; mon->dim.px_real.height = height; output_update_ppi(mon); } static void output_done(void *data, struct wl_output *wl_output) { struct monitor *mon = data; update_terms_on_monitor(mon); } static void output_scale(void *data, struct wl_output *wl_output, int32_t factor) { struct monitor *mon = data; mon->scale = factor; output_update_ppi(mon); } #if defined(WL_OUTPUT_NAME_SINCE_VERSION) static void output_name(void *data, struct wl_output *wl_output, const char *name) { struct monitor *mon = data; free(mon->name); mon->name = name != NULL ? xstrdup(name) : NULL; } #endif #if defined(WL_OUTPUT_DESCRIPTION_SINCE_VERSION) static void output_description(void *data, struct wl_output *wl_output, const char *description) { struct monitor *mon = data; free(mon->description); mon->description = description != NULL ? xstrdup(description) : NULL; } #endif static const struct wl_output_listener output_listener = { .geometry = &output_geometry, .mode = &output_mode, .done = &output_done, .scale = &output_scale, #if defined(WL_OUTPUT_NAME_SINCE_VERSION) .name = &output_name, #endif #if defined(WL_OUTPUT_DESCRIPTION_SINCE_VERSION) .description = &output_description, #endif }; static void xdg_output_handle_logical_position( void *data, struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y) { struct monitor *mon = data; mon->x = x; mon->y = y; } static void xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) { struct monitor *mon = data; mon->dim.px_scaled.width = width; mon->dim.px_scaled.height = height; output_update_ppi(mon); } static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) { struct monitor *mon = data; update_terms_on_monitor(mon); } static void xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) { struct monitor *mon = data; free(mon->name); mon->name = name != NULL ? xstrdup(name) : NULL; } static void xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) { struct monitor *mon = data; free(mon->description); mon->description = description != NULL ? xstrdup(description) : NULL; } static const struct zxdg_output_v1_listener xdg_output_listener = { .logical_position = xdg_output_handle_logical_position, .logical_size = xdg_output_handle_logical_size, .done = xdg_output_handle_done, .name = xdg_output_handle_name, .description = xdg_output_handle_description, }; static void clock_id(void *data, struct wp_presentation *wp_presentation, uint32_t clk_id) { struct wayland *wayl = data; wayl->presentation_clock_id = clk_id; LOG_DBG("presentation clock ID: %u", clk_id); } static const struct wp_presentation_listener presentation_listener = { .clock_id = &clock_id, }; static void color_manager_create_image_description(struct wayland *wayl) { if (!wayl->color_management.have_feat_parametric) return; if (!wayl->color_management.have_primaries_srgb) return; if (!wayl->color_management.have_tf_ext_linear) return; struct wp_image_description_creator_params_v1 *params = wp_color_manager_v1_create_parametric_creator(wayl->color_management.manager); wp_image_description_creator_params_v1_set_tf_named( params, WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR); wp_image_description_creator_params_v1_set_primaries_named( params, WP_COLOR_MANAGER_V1_PRIMARIES_SRGB); wayl->color_management.img_description = wp_image_description_creator_params_v1_create(params); } static void color_manager_supported_intent(void *data, struct wp_color_manager_v1 *wp_color_manager_v1, uint32_t render_intent) { struct wayland *wayl = data; if (render_intent == WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL) wayl->color_management.have_intent_perceptual = true; } static void color_manager_supported_feature(void *data, struct wp_color_manager_v1 *wp_color_manager_v1, uint32_t feature) { struct wayland *wayl = data; if (feature == WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC) wayl->color_management.have_feat_parametric = true; } static void color_manager_supported_tf_named(void *data, struct wp_color_manager_v1 *wp_color_manager_v1, uint32_t tf) { struct wayland *wayl = data; if (tf == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR) wayl->color_management.have_tf_ext_linear = true; } static void color_manager_supported_primaries_named(void *data, struct wp_color_manager_v1 *wp_color_manager_v1, uint32_t primaries) { struct wayland *wayl = data; if (primaries == WP_COLOR_MANAGER_V1_PRIMARIES_SRGB) wayl->color_management.have_primaries_srgb = true; } static void color_manager_done(void *data, struct wp_color_manager_v1 *wp_color_manager_v1) { struct wayland *wayl = data; color_manager_create_image_description(wayl); } static const struct wp_color_manager_v1_listener color_manager_listener = { .supported_intent = &color_manager_supported_intent, .supported_feature = &color_manager_supported_feature, .supported_primaries_named = &color_manager_supported_primaries_named, .supported_tf_named = &color_manager_supported_tf_named, .done = &color_manager_done, }; static bool verify_iface_version(const char *iface, uint32_t version, uint32_t wanted) { if (version >= wanted) return true; LOG_ERR("%s: need interface version %u, but compositor only implements %u", iface, wanted, version); return false; } static void surface_enter(void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) { struct wl_window *win = data; struct terminal *term = win->term; tll_foreach(term->wl->monitors, it) { if (it->item.output == wl_output) { LOG_DBG("mapped on %s", it->item.name); tll_push_back(term->window->on_outputs, &it->item); update_term_for_output_change(term); return; } } LOG_ERR("mapped on unknown output"); } static void surface_leave(void *data, struct wl_surface *wl_surface, struct wl_output *wl_output) { struct wl_window *win = data; struct terminal *term = win->term; tll_foreach(term->window->on_outputs, it) { if (it->item->output != wl_output) continue; LOG_DBG("unmapped from %s", it->item->name); tll_remove(term->window->on_outputs, it); update_term_for_output_change(term); return; } LOG_WARN("unmapped from unknown output"); } #if defined(WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) static void surface_preferred_buffer_scale(void *data, struct wl_surface *surface, int32_t scale) { struct wl_window *win = data; if (win->preferred_buffer_scale == scale) return; LOG_DBG("wl_surface preferred scale: %d -> %d", win->preferred_buffer_scale, scale); win->preferred_buffer_scale = scale; update_term_for_output_change(win->term); } static void surface_preferred_buffer_transform(void *data, struct wl_surface *surface, uint32_t transform) { } #endif static const struct wl_surface_listener surface_listener = { .enter = &surface_enter, .leave = &surface_leave, #if defined(WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) .preferred_buffer_scale = &surface_preferred_buffer_scale, .preferred_buffer_transform = &surface_preferred_buffer_transform, #endif }; static void xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height, struct wl_array *states) { bool is_activated = false; bool is_fullscreen = false; bool is_maximized = false; bool is_resizing = false; bool is_tiled_top = false; bool is_tiled_bottom = false; bool is_tiled_left = false; bool is_tiled_right = false; bool is_suspended UNUSED = false; #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG char state_str[2048]; int state_chars = 0; static const char *const strings[] = { [XDG_TOPLEVEL_STATE_MAXIMIZED] = "maximized", [XDG_TOPLEVEL_STATE_FULLSCREEN] = "fullscreen", [XDG_TOPLEVEL_STATE_RESIZING] = "resizing", [XDG_TOPLEVEL_STATE_ACTIVATED] = "activated", [XDG_TOPLEVEL_STATE_TILED_LEFT] = "tiled:left", [XDG_TOPLEVEL_STATE_TILED_RIGHT] = "tiled:right", [XDG_TOPLEVEL_STATE_TILED_TOP] = "tiled:top", [XDG_TOPLEVEL_STATE_TILED_BOTTOM] = "tiled:bottom", #if defined(XDG_TOPLEVEL_STATE_SUSPENDED_SINCE_VERSION) /* wayland-protocols >= 1.32 */ [XDG_TOPLEVEL_STATE_SUSPENDED] = "suspended", #endif }; #endif enum xdg_toplevel_state *state; wl_array_for_each(state, states) { switch (*state) { case XDG_TOPLEVEL_STATE_MAXIMIZED: is_maximized = true; break; case XDG_TOPLEVEL_STATE_FULLSCREEN: is_fullscreen = true; break; case XDG_TOPLEVEL_STATE_RESIZING: is_resizing = true; break; case XDG_TOPLEVEL_STATE_ACTIVATED: is_activated = true; break; case XDG_TOPLEVEL_STATE_TILED_LEFT: is_tiled_left = true; break; case XDG_TOPLEVEL_STATE_TILED_RIGHT: is_tiled_right = true; break; case XDG_TOPLEVEL_STATE_TILED_TOP: is_tiled_top = true; break; case XDG_TOPLEVEL_STATE_TILED_BOTTOM: is_tiled_bottom = true; break; #if defined(XDG_TOPLEVEL_STATE_SUSPENDED_SINCE_VERSION) case XDG_TOPLEVEL_STATE_SUSPENDED: is_suspended = true; break; #endif } #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG if (*state >= 0 && *state < ALEN(strings)) { state_chars += snprintf( &state_str[state_chars], sizeof(state_str) - state_chars, "%s, ", strings[*state] != NULL ? strings[*state] : ""); } #endif } #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG if (state_chars > 2) state_str[state_chars - 2] = '\0'; else state_str[0] = '\0'; LOG_DBG("xdg-toplevel: configure: size=%dx%d, states=[%s]", width, height, state_str); #endif /* * Changes done here are ignored until the configure event has * been ack:ed in xdg_surface_configure(). * * So, just store the config data and apply it later, in * xdg_surface_configure() after we've ack:ed the event. */ struct wl_window *win = data; win->configure.is_activated = is_activated; win->configure.is_fullscreen = is_fullscreen; win->configure.is_maximized = is_maximized; win->configure.is_resizing = is_resizing; win->configure.is_tiled_top = is_tiled_top; win->configure.is_tiled_bottom = is_tiled_bottom; win->configure.is_tiled_left = is_tiled_left; win->configure.is_tiled_right = is_tiled_right; win->configure.width = width; win->configure.height = height; } static void xdg_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel) { struct wl_window *win = data; struct terminal *term = win->term; LOG_DBG("xdg-toplevel: close"); term_shutdown(term); } #if defined(XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION) static void xdg_toplevel_configure_bounds(void *data, struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) { /* TODO: ensure we don't pick a bigger size */ } #endif #if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) static void xdg_toplevel_wm_capabilities(void *data, struct xdg_toplevel *xdg_toplevel, struct wl_array *caps) { struct wl_window *win = data; win->wm_capabilities.maximize = false; win->wm_capabilities.minimize = false; #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG char cap_str[2048]; int cap_chars = 0; static const char *const strings[] = { [XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU] = "window-menu", [XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE] = "maximize", [XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN] = "fullscreen", [XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE] = "minimize", }; #endif enum xdg_toplevel_wm_capabilities *cap; wl_array_for_each(cap, caps) { switch (*cap) { case XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE: win->wm_capabilities.maximize = true; break; case XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE: win->wm_capabilities.minimize = true; break; case XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU: case XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN: break; } #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG if (*cap >= 0 && *cap < ALEN(strings)) { cap_chars += snprintf( &cap_str[cap_chars], sizeof(cap_str) - cap_chars, "%s, ", strings[*cap] != NULL ? strings[*cap] : ""); } #endif } #if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG if (cap_chars > 2) cap_str[cap_chars - 2] = '\0'; else cap_str[0] = '\0'; LOG_DBG("xdg-toplevel: wm-capabilities=[%s]", cap_str); #endif } #endif static const struct xdg_toplevel_listener xdg_toplevel_listener = { .configure = &xdg_toplevel_configure, /*.close = */&xdg_toplevel_close, /* epoll-shim defines a macro 'close'... */ #if defined(XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION) .configure_bounds = &xdg_toplevel_configure_bounds, #endif #if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) .wm_capabilities = xdg_toplevel_wm_capabilities, #endif }; static void xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial) { LOG_DBG("xdg-surface: configure"); struct wl_window *win = data; struct terminal *term = win->term; if (win->unmapped) { /* * https://codeberg.org/dnkl/foot/issues/1249 * https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/3487 * https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/3719 * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/108 */ return; } bool wasnt_configured = !win->is_configured; bool was_resizing = win->is_resizing; bool csd_was_enabled = win->csd_mode == CSD_YES && !win->is_fullscreen; int new_width = win->configure.width; int new_height = win->configure.height; win->is_configured = true; win->is_maximized = win->configure.is_maximized; win->is_fullscreen = win->configure.is_fullscreen; win->is_resizing = win->configure.is_resizing; win->is_tiled_top = win->configure.is_tiled_top; win->is_tiled_bottom = win->configure.is_tiled_bottom; win->is_tiled_left = win->configure.is_tiled_left; win->is_tiled_right = win->configure.is_tiled_right; win->is_tiled = (win->is_tiled_top || win->is_tiled_bottom || win->is_tiled_left || win->is_tiled_right); win->csd_mode = win->configure.csd_mode; bool enable_csd = win->csd_mode == CSD_YES && !win->is_fullscreen; if (!csd_was_enabled && enable_csd) csd_instantiate(win); else if (csd_was_enabled && !enable_csd) csd_destroy(win); if (enable_csd && new_width > 0 && new_height > 0) { if (wayl_win_csd_titlebar_visible(win)) new_height -= win->term->conf->csd.title_height; if (wayl_win_csd_borders_visible(win)) { new_height -= 2 * win->term->conf->csd.border_width_visible; new_width -= 2 * win->term->conf->csd.border_width_visible; } } xdg_surface_ack_configure(xdg_surface, serial); enum resize_options opts = RESIZE_BY_CELLS; #if 1 /* * TODO: decide if we should do the last "forced" call when ending * an interactive resize. * * Without it, the last TIOCSWINSZ sent to the client will be a * scheduled one. I.e. there will be a small delay after the user * has *stopped* resizing, and the client application receives the * final size. * * Note: if we also disable content centering while resizing, then * the last, forced, resize *is* necessary. */ if (was_resizing && !win->is_resizing) opts |= RESIZE_FORCE; #endif bool resized = render_resize(term, new_width, new_height, opts); if (win->configure.is_activated) term_visual_focus_in(term); else term_visual_focus_out(term); if (!resized) { /* * 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. */ wl_surface_commit(win->surface.surf); } if (wasnt_configured) term_window_configured(term); } static const struct xdg_surface_listener xdg_surface_listener = { .configure = &xdg_surface_configure, }; static void xdg_toplevel_decoration_configure(void *data, struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, uint32_t mode) { struct wl_window *win = data; xassert(win->term->conf->csd.preferred != CONF_CSD_PREFER_NONE); switch (mode) { case ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE: LOG_INFO("using CSD decorations"); win->configure.csd_mode = CSD_YES; break; case ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE: LOG_INFO("using SSD decorations"); win->configure.csd_mode = CSD_NO; break; default: LOG_ERR("unimplemented: unknown XDG toplevel decoration mode: %u", mode); break; } } static const struct zxdg_toplevel_decoration_v1_listener xdg_toplevel_decoration_listener = { .configure = &xdg_toplevel_decoration_configure, }; static bool fdm_repeat(struct fdm *fdm, int fd, int events, void *data) { if (events & EPOLLHUP) return false; struct seat *seat = data; uint64_t expiration_count; ssize_t ret = read( seat->kbd.repeat.fd, &expiration_count, sizeof(expiration_count)); if (ret < 0) { if (errno == EAGAIN) return true; LOG_ERRNO("failed to read repeat key from repeat timer fd"); return false; } seat->kbd.repeat.dont_re_repeat = true; for (size_t i = 0; i < expiration_count; i++) input_repeat(seat, seat->kbd.repeat.key); seat->kbd.repeat.dont_re_repeat = false; return true; } static void handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { LOG_DBG("global: 0x%08x, interface=%s, version=%u", name, interface, version); struct wayland *wayl = data; if (streq(interface, wl_compositor_interface.name)) { const uint32_t required = 4; if (!verify_iface_version(interface, version, required)) return; #if defined (WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) const uint32_t preferred = WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION; #else const uint32_t preferred = required; #endif wayl->compositor = wl_registry_bind( wayl->registry, name, &wl_compositor_interface, min(version, preferred)); } else if (streq(interface, wl_subcompositor_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->sub_compositor = wl_registry_bind( wayl->registry, name, &wl_subcompositor_interface, required); } else if (streq(interface, wl_shm_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; #if defined(WL_SHM_RELEASE_SINCE_VERSION) const uint32_t preferred = WL_SHM_RELEASE_SINCE_VERSION; #else const uint32_t preferred = required; #endif wayl->shm = wl_registry_bind( wayl->registry, name, &wl_shm_interface, min(version, preferred)); wl_shm_add_listener(wayl->shm, &shm_listener, wayl); #if defined(WL_SHM_RELEASE_SINCE_VERSION) wayl->use_shm_release = version >= WL_SHM_RELEASE_SINCE_VERSION; #else wayl->use_shm_release = false; #endif } else if (streq(interface, xdg_wm_base_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; /* * We *require* version 1, but _can_ use version 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. */ #if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) const uint32_t preferred = XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION; #elif defined(XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION) const uint32_t preferred = XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION; #else const uint32_t preferred = required; #endif wayl->shell = wl_registry_bind( wayl->registry, name, &xdg_wm_base_interface, min(version, preferred)); xdg_wm_base_add_listener(wayl->shell, &xdg_wm_base_listener, wayl); } else if (streq(interface, zxdg_decoration_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->xdg_decoration_manager = wl_registry_bind( wayl->registry, name, &zxdg_decoration_manager_v1_interface, required); } else if (streq(interface, wl_seat_interface.name)) { const uint32_t required = 5; if (!verify_iface_version(interface, version, required)) return; #if defined(WL_POINTER_AXIS_VALUE120_SINCE_VERSION) const uint32_t preferred = WL_POINTER_AXIS_VALUE120_SINCE_VERSION; #else const uint32_t preferred = required; #endif int repeat_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (repeat_fd == -1) { LOG_ERRNO("failed to create keyboard repeat timer FD"); return; } struct wl_seat *wl_seat = wl_registry_bind( wayl->registry, name, &wl_seat_interface, min(version, preferred)); tll_push_back(wayl->seats, ((struct seat){ .wayl = wayl, .wl_seat = wl_seat, .wl_name = name, .kbd = { .repeat = { .fd = repeat_fd, }, }})); struct seat *seat = &tll_back(wayl->seats); if (!fdm_add(wayl->fdm, repeat_fd, EPOLLIN, &fdm_repeat, seat)) { close(repeat_fd); seat->kbd.repeat.fd = -1; seat_destroy(seat); return; } seat->kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); if (seat->kbd.xkb != NULL) { seat->kbd.xkb_compose_table = xkb_compose_table_new_from_locale( seat->kbd.xkb, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); if (seat->kbd.xkb_compose_table != NULL) { seat->kbd.xkb_compose_state = xkb_compose_state_new( seat->kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); } else { LOG_WARN("failed to instantiate compose table; dead keys (compose) will not work"); } } seat_add_data_device(seat); seat_add_primary_selection(seat); seat_add_text_input(seat); seat_add_key_bindings(seat); wl_seat_add_listener(wl_seat, &seat_listener, seat); } else if (streq(interface, zxdg_output_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->xdg_output_manager = wl_registry_bind( wayl->registry, name, &zxdg_output_manager_v1_interface, min(version, 2)); tll_foreach(wayl->monitors, it) { struct monitor *mon = &it->item; mon->xdg = zxdg_output_manager_v1_get_xdg_output( wayl->xdg_output_manager, mon->output); zxdg_output_v1_add_listener(mon->xdg, &xdg_output_listener, mon); } } else if (streq(interface, wl_output_interface.name)) { const uint32_t required = 2; if (!verify_iface_version(interface, version, required)) return; #if defined(WL_OUTPUT_NAME_SINCE_VERSION) const uint32_t preferred = WL_OUTPUT_NAME_SINCE_VERSION; #elif defined(WL_OUTPUT_RELEASE_SINCE_VERSION) const uint32_t preferred = WL_OUTPUT_RELEASE_SINCE_VERSION; #else const uint32_t preferred = required; #endif struct wl_output *output = wl_registry_bind( wayl->registry, name, &wl_output_interface, min(version, preferred)); tll_push_back( wayl->monitors, ((struct monitor){.wayl = wayl, .output = output, .wl_name = name, .scale = 1, .use_output_release = version >= WL_OUTPUT_RELEASE_SINCE_VERSION})); struct monitor *mon = &tll_back(wayl->monitors); wl_output_add_listener(output, &output_listener, mon); if (wayl->xdg_output_manager != NULL) { mon->xdg = zxdg_output_manager_v1_get_xdg_output( wayl->xdg_output_manager, mon->output); zxdg_output_v1_add_listener(mon->xdg, &xdg_output_listener, mon); } } else if (streq(interface, wl_data_device_manager_interface.name)) { const uint32_t required = 3; if (!verify_iface_version(interface, version, required)) return; wayl->data_device_manager = wl_registry_bind( wayl->registry, name, &wl_data_device_manager_interface, required); tll_foreach(wayl->seats, it) seat_add_data_device(&it->item); } else if (streq(interface, zwp_primary_selection_device_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->primary_selection_device_manager = wl_registry_bind( wayl->registry, name, &zwp_primary_selection_device_manager_v1_interface, required); tll_foreach(wayl->seats, it) seat_add_primary_selection(&it->item); } else if (streq(interface, wp_presentation_interface.name)) { if (wayl->presentation_timings) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->presentation = wl_registry_bind( wayl->registry, name, &wp_presentation_interface, required); wp_presentation_add_listener( wayl->presentation, &presentation_listener, wayl); } } else if (streq(interface, xdg_activation_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->xdg_activation = wl_registry_bind( wayl->registry, name, &xdg_activation_v1_interface, required); } else if (streq(interface, wp_viewporter_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->viewporter = wl_registry_bind( wayl->registry, name, &wp_viewporter_interface, required); } else if (streq(interface, wp_fractional_scale_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->fractional_scale_manager = wl_registry_bind( wayl->registry, name, &wp_fractional_scale_manager_v1_interface, required); } else if (streq(interface, wp_cursor_shape_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->cursor_shape_manager = wl_registry_bind( wayl->registry, name, &wp_cursor_shape_manager_v1_interface, required); } 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_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_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(FOOT_IME_ENABLED) && FOOT_IME_ENABLED else if (streq(interface, zwp_text_input_manager_v3_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; wayl->text_input_manager = wl_registry_bind( wayl->registry, name, &zwp_text_input_manager_v3_interface, required); tll_foreach(wayl->seats, it) seat_add_text_input(&it->item); } #endif } static void monitor_destroy(struct monitor *mon) { if (mon->xdg != NULL) zxdg_output_v1_destroy(mon->xdg); if (mon->output != NULL) { if (mon->use_output_release) wl_output_release(mon->output); else wl_output_destroy(mon->output); } free(mon->make); free(mon->model); free(mon->name); free(mon->description); } static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { LOG_DBG("global removed: 0x%08x", name); struct wayland *wayl = data; /* Check if this is an output */ tll_foreach(wayl->monitors, it) { struct monitor *mon = &it->item; if (mon->wl_name != name) continue; LOG_INFO("monitor unplugged or disabled: %s", mon->name); /* * Update all terminals that are mapped here. On Sway 1.4, * surfaces are *not* unmapped before the output is removed */ tll_foreach(wayl->terms, t) { tll_foreach(t->item->window->on_outputs, o) { if (o->item->output == mon->output) { surface_leave(t->item->window, NULL, mon->output); break; } } } monitor_destroy(mon); tll_remove(wayl->monitors, it); return; } /* A seat? */ tll_foreach(wayl->seats, it) { struct seat *seat = &it->item; if (seat->wl_name != name) continue; LOG_INFO("seat destroyed: %s", seat->name); if (seat->kbd_focus != NULL) { LOG_WARN("compositor destroyed seat '%s' " "without sending a keyboard leave event", seat->name); if (seat->wl_keyboard != NULL) keyboard_listener.leave( seat, seat->wl_keyboard, -1, seat->kbd_focus->window->surface.surf); } if (seat->mouse_focus != NULL) { LOG_WARN("compositor destroyed seat '%s' " "without sending a pointer leave event", seat->name); if (seat->wl_pointer != NULL) pointer_listener.leave( seat, seat->wl_pointer, -1, seat->mouse_focus->window->surface.surf); } seat_destroy(seat); tll_remove(wayl->seats, it); return; } LOG_WARN("unknown global removed: 0x%08x", name); } static const struct wl_registry_listener registry_listener = { .global = &handle_global, .global_remove = &handle_global_remove, }; static void fdm_hook(struct fdm *fdm, void *data) { struct wayland *wayl = data; wayl_flush(wayl); } static bool fdm_wayl(struct fdm *fdm, int fd, int events, void *data) { struct wayland *wayl = data; int event_count = 0; if (events & EPOLLIN) { if (wl_display_read_events(wayl->display) < 0) { LOG_ERRNO("failed to read events from the Wayland socket"); return false; } while (wl_display_prepare_read(wayl->display) != 0) { if (wl_display_dispatch_pending(wayl->display) < 0) { LOG_ERRNO("failed to dispatch pending Wayland events"); return false; } } } if (events & EPOLLHUP) { LOG_WARN("disconnected from Wayland"); /* * Do *not* call wl_display_cancel_read() here. * * Doing so causes later calls to wayl_roundtrip() (called * from term_destroy() -> wayl_win_destroy()) to hang * indefinitely. * * https://codeberg.org/dnkl/foot/issues/651 */ return false; } return event_count != -1; } struct wayland * wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, bool presentation_timings) { struct wayland *wayl = calloc(1, sizeof(*wayl)); if (unlikely(wayl == NULL)) { LOG_ERRNO("calloc() failed"); return NULL; } wayl->fdm = fdm; wayl->key_binding_manager = key_binding_manager; wayl->fd = -1; wayl->presentation_timings = presentation_timings; if (!fdm_hook_add(fdm, &fdm_hook, wayl, FDM_HOOK_PRIORITY_LOW)) { LOG_ERR("failed to add FDM hook"); goto out; } wayl->display = wl_display_connect(NULL); if (wayl->display == NULL) { LOG_ERR("failed to connect to wayland; no compositor running?"); goto out; } wayl->registry = wl_display_get_registry(wayl->display); if (wayl->registry == NULL) { LOG_ERR("failed to get wayland registry"); goto out; } wl_registry_add_listener(wayl->registry, ®istry_listener, wayl); wl_display_roundtrip(wayl->display); if (wayl->compositor == NULL) { LOG_ERR("no compositor"); goto out; } if (wayl->sub_compositor == NULL) { LOG_ERR("no sub compositor"); goto out; } if (wayl->shm == NULL) { LOG_ERR("no shared memory buffers interface"); goto out; } if (wayl->shell == NULL) { LOG_ERR("no XDG shell interface"); goto out; } if (wayl->data_device_manager == NULL) { LOG_ERR("no clipboard available " "(wl_data_device_manager not implemented by server)"); goto out; } if (tll_length(wayl->seats) == 0) { LOG_ERR("no seats available (wl_seat interface too old?)"); goto out; } if (tll_length(wayl->monitors) == 0) { LOG_ERR("no monitors available"); goto out; } if (presentation_timings && wayl->presentation == NULL) { LOG_ERR("compositor does not implement the presentation time interface"); goto out; } if (wayl->primary_selection_device_manager == NULL) LOG_WARN("compositor does not implement the primary selection interface"); if (wayl->xdg_activation == NULL) { LOG_WARN( "compositor does not implement XDG activation, " "bell.urgent will fall back to coloring the window margins red"); } if (wayl->fractional_scale_manager == NULL || wayl->viewporter == NULL) LOG_WARN("compositor does not implement fractional scaling"); if (wayl->cursor_shape_manager == NULL) { LOG_WARN("compositor does not implement server-side cursors, " "falling back to client-side cursors"); } if (wayl->toplevel_icon_manager == NULL) { LOG_WARN("compositor does not implement the XDG toplevel icon protocol"); } #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (wayl->text_input_manager == NULL) { LOG_WARN("text input interface not implemented by compositor; " "IME will be disabled"); } #endif /* Trigger listeners registered when handling globals */ wl_display_roundtrip(wayl->display); tll_foreach(wayl->monitors, it) { LOG_INFO( "%s: %dx%d+%dx%d@%dHz %s %.2f\" scale=%d, DPI=%.2f/%.2f (physical/scaled)", it->item.name, it->item.dim.px_real.width, it->item.dim.px_real.height, it->item.x, it->item.y, (int)roundf(it->item.refresh), it->item.model != NULL ? it->item.model : it->item.description, it->item.inch, it->item.scale, it->item.dpi.physical, it->item.dpi.scaled); } wayl->fd = wl_display_get_fd(wayl->display); if (fcntl(wayl->fd, F_SETFL, fcntl(wayl->fd, F_GETFL) | O_NONBLOCK) < 0) { LOG_ERRNO("failed to make Wayland socket non-blocking"); goto out; } if (!fdm_add(fdm, wayl->fd, EPOLLIN, &fdm_wayl, wayl)) goto out; if (wl_display_prepare_read(wayl->display) != 0) { LOG_ERRNO("failed to prepare for reading wayland events"); goto out; } return wayl; out: if (wayl != NULL) wayl_destroy(wayl); return NULL; } void wayl_destroy(struct wayland *wayl) { if (wayl == NULL) return; tll_foreach(wayl->terms, it) { static bool have_warned = false; if (!have_warned) { have_warned = true; LOG_WARN("there are terminals still running"); term_destroy(it->item); } } tll_free(wayl->terms); fdm_hook_del(wayl->fdm, &fdm_hook, FDM_HOOK_PRIORITY_LOW); tll_foreach(wayl->monitors, it) { monitor_destroy(&it->item); tll_remove(wayl->monitors, it); } tll_foreach(wayl->seats, it) { seat_destroy(&it->item); tll_remove(wayl->seats, it); } #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (wayl->text_input_manager != NULL) zwp_text_input_manager_v3_destroy(wayl->text_input_manager); #endif if (wayl->color_management.img_description != NULL) wp_image_description_v1_destroy(wayl->color_management.img_description); if (wayl->color_management.manager != NULL) wp_color_manager_v1_destroy(wayl->color_management.manager); if (wayl->system_bell != NULL) xdg_system_bell_v1_destroy(wayl->system_bell); if (wayl->toplevel_icon_manager != NULL) xdg_toplevel_icon_manager_v1_destroy(wayl->toplevel_icon_manager); if (wayl->single_pixel_manager != NULL) wp_single_pixel_buffer_manager_v1_destroy(wayl->single_pixel_manager); if (wayl->fractional_scale_manager != NULL) wp_fractional_scale_manager_v1_destroy(wayl->fractional_scale_manager); if (wayl->viewporter != NULL) wp_viewporter_destroy(wayl->viewporter); if (wayl->cursor_shape_manager != NULL) wp_cursor_shape_manager_v1_destroy(wayl->cursor_shape_manager); if (wayl->xdg_activation != NULL) xdg_activation_v1_destroy(wayl->xdg_activation); if (wayl->xdg_output_manager != NULL) zxdg_output_manager_v1_destroy(wayl->xdg_output_manager); if (wayl->shell != NULL) xdg_wm_base_destroy(wayl->shell); if (wayl->xdg_decoration_manager != NULL) zxdg_decoration_manager_v1_destroy(wayl->xdg_decoration_manager); if (wayl->presentation != NULL) wp_presentation_destroy(wayl->presentation); if (wayl->data_device_manager != NULL) wl_data_device_manager_destroy(wayl->data_device_manager); if (wayl->primary_selection_device_manager != NULL) zwp_primary_selection_device_manager_v1_destroy(wayl->primary_selection_device_manager); if (wayl->shm != NULL) { #if defined(WL_SHM_RELEASE_SINCE_VERSION) if (wayl->use_shm_release) wl_shm_release(wayl->shm); else #endif wl_shm_destroy(wayl->shm); } if (wayl->sub_compositor != NULL) wl_subcompositor_destroy(wayl->sub_compositor); if (wayl->compositor != NULL) wl_compositor_destroy(wayl->compositor); if (wayl->registry != NULL) wl_registry_destroy(wayl->registry); if (wayl->fd != -1) fdm_del_no_close(wayl->fdm, wayl->fd); if (wayl->display != NULL) { wayl_flush(wayl); wl_display_disconnect(wayl->display); } free(wayl); } static void fractional_scale_preferred_scale( void *data, struct wp_fractional_scale_v1 *wp_fractional_scale_v1, uint32_t scale) { struct wl_window *win = data; const float new_scale = (float)scale / 120.; if (win->scale == new_scale) return; LOG_DBG("fractional scale: %.2f -> %.2f", win->scale, new_scale); win->scale = new_scale; update_term_for_output_change(win->term); } static const struct wp_fractional_scale_v1_listener fractional_scale_listener = { .preferred_scale = &fractional_scale_preferred_scale, }; struct wl_window * wayl_win_init(struct terminal *term, const char *token) { struct wayland *wayl = term->wl; const struct config *conf = term->conf; struct wl_window *win = calloc(1, sizeof(*win)); if (unlikely(win == NULL)) { LOG_ERRNO("calloc() failed"); return NULL; } win->term = term; win->csd_mode = CSD_UNKNOWN; win->csd.move_timeout_fd = -1; win->resize_timeout_fd = -1; win->scale = -1.; win->wm_capabilities.maximize = true; win->wm_capabilities.minimize = true; win->surface.surf = wl_compositor_create_surface(wayl->compositor); if (win->surface.surf == NULL) { LOG_ERR("failed to create wayland surface"); goto out; } wayl_win_alpha_changed(win); wl_surface_add_listener(win->surface.surf, &surface_listener, win); if (wayl->fractional_scale_manager != NULL && wayl->viewporter != NULL) { win->surface.viewport = wp_viewporter_get_viewport(wayl->viewporter, win->surface.surf); win->fractional_scale = wp_fractional_scale_manager_v1_get_fractional_scale( wayl->fractional_scale_manager, win->surface.surf); wp_fractional_scale_v1_add_listener( win->fractional_scale, &fractional_scale_listener, win); } win->xdg_surface = xdg_wm_base_get_xdg_surface(wayl->shell, win->surface.surf); xdg_surface_add_listener(win->xdg_surface, &xdg_surface_listener, win); win->xdg_toplevel = xdg_surface_get_toplevel(win->xdg_surface); xdg_toplevel_add_listener(win->xdg_toplevel, &xdg_toplevel_listener, win); xdg_toplevel_set_app_id(win->xdg_toplevel, conf->app_id); if (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 != GAMMA_CORRECT_DISABLED) { 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 (term->conf->gamma_correct == GAMMA_CORRECT_ENABLED) { 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"); } } else { /* "auto" - don't warn */ } } if (conf->csd.preferred == CONF_CSD_PREFER_NONE) { /* User specifically do *not* want decorations */ win->csd_mode = CSD_NO; LOG_INFO("window decorations disabled by user"); } else if (wayl->xdg_decoration_manager != NULL) { win->xdg_toplevel_decoration = zxdg_decoration_manager_v1_get_toplevel_decoration( wayl->xdg_decoration_manager, win->xdg_toplevel); LOG_INFO("requesting %s decorations", conf->csd.preferred == CONF_CSD_PREFER_SERVER ? "SSD" : "CSD"); zxdg_toplevel_decoration_v1_set_mode( win->xdg_toplevel_decoration, (conf->csd.preferred == CONF_CSD_PREFER_SERVER ? ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE : ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE)); zxdg_toplevel_decoration_v1_add_listener( win->xdg_toplevel_decoration, &xdg_toplevel_decoration_listener, win); } else { /* No decoration manager - thus we *must* draw our own decorations */ win->configure.csd_mode = CSD_YES; LOG_WARN("no decoration manager available - using CSDs unconditionally"); } wl_surface_commit(win->surface.surf); /* Complete XDG startup notification */ wayl_activate(wayl, win, token); if (!wayl_win_subsurface_new(win, &win->overlay, false)) { LOG_ERR("failed to create overlay surface"); goto out; } switch (conf->tweak.render_timer) { case RENDER_TIMER_OSD: case RENDER_TIMER_BOTH: if (!wayl_win_subsurface_new(win, &win->render_timer, false)) { LOG_ERR("failed to create render timer surface"); goto out; } break; case RENDER_TIMER_NONE: case RENDER_TIMER_LOG: break; } return win; out: if (win != NULL) wayl_win_destroy(win); return NULL; } void wayl_win_destroy(struct wl_window *win) { if (win == NULL) return; struct terminal *term = win->term; if (win->csd.move_timeout_fd != -1) close(win->csd.move_timeout_fd); /* * First, unmap all surfaces to trigger things like * keyboard_leave() and wl_pointer_leave(). * * This ensures we remove all references to *this* window from the * global wayland struct (since it no longer has neither keyboard * nor mouse focus). */ if (win->render_timer.surface.surf != NULL) { wl_surface_attach(win->render_timer.surface.surf, NULL, 0, 0); wl_surface_commit(win->render_timer.surface.surf); } if (win->scrollback_indicator.surface.surf != NULL) { wl_surface_attach(win->scrollback_indicator.surface.surf, NULL, 0, 0); wl_surface_commit(win->scrollback_indicator.surface.surf); } /* Scrollback search */ if (win->search.surface.surf != NULL) { wl_surface_attach(win->search.surface.surf, NULL, 0, 0); wl_surface_commit(win->search.surface.surf); } /* URLs */ tll_foreach(win->urls, it) { wl_surface_attach(it->item.surf.surface.surf, NULL, 0, 0); wl_surface_commit(it->item.surf.surface.surf); } /* CSD */ for (size_t i = 0; i < ALEN(win->csd.surface); i++) { if (win->csd.surface[i].surface.surf != NULL) { wl_surface_attach(win->csd.surface[i].surface.surf, NULL, 0, 0); wl_surface_commit(win->csd.surface[i].surface.surf); } } wayl_roundtrip(win->term->wl); /* Main window */ win->unmapped = true; wl_surface_attach(win->surface.surf, NULL, 0, 0); wl_surface_commit(win->surface.surf); wayl_roundtrip(win->term->wl); tll_free(win->on_outputs); tll_foreach(win->urls, it) { wayl_win_subsurface_destroy(&it->item.surf); tll_remove(win->urls, it); } csd_destroy(win); wayl_win_subsurface_destroy(&win->search); wayl_win_subsurface_destroy(&win->scrollback_indicator); wayl_win_subsurface_destroy(&win->render_timer); wayl_win_subsurface_destroy(&win->overlay); shm_purge(term->render.chains.search); shm_purge(term->render.chains.scrollback_indicator); shm_purge(term->render.chains.render_timer); shm_purge(term->render.chains.grid); shm_purge(term->render.chains.url); shm_purge(term->render.chains.csd); 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 (win->surface.color_management != NULL) wp_color_management_surface_v1_destroy(win->surface.color_management); if (win->fractional_scale != NULL) wp_fractional_scale_v1_destroy(win->fractional_scale); if (win->surface.viewport != NULL) wp_viewport_destroy(win->surface.viewport); if (win->frame_callback != NULL) wl_callback_destroy(win->frame_callback); if (win->xdg_toplevel_decoration != NULL) zxdg_toplevel_decoration_v1_destroy(win->xdg_toplevel_decoration); if (win->xdg_toplevel != NULL) xdg_toplevel_destroy(win->xdg_toplevel); if (win->xdg_surface != NULL) xdg_surface_destroy(win->xdg_surface); if (win->surface.surf != NULL) wl_surface_destroy(win->surface.surf); wayl_roundtrip(win->term->wl); if (win->resize_timeout_fd >= 0) fdm_del(win->term->wl->fdm, win->resize_timeout_fd); free(win); } bool wayl_reload_xcursor_theme(struct seat *seat, float new_scale) { if (seat->pointer.theme != NULL && seat->pointer.scale == new_scale) { /* We already have a theme loaded, and the scale hasn't changed */ return true; } if (seat->pointer.theme != NULL) { xassert(seat->pointer.scale != new_scale); wl_cursor_theme_destroy(seat->pointer.theme); seat->pointer.theme = NULL; seat->pointer.cursor = NULL; } if (seat->pointer.shape_device != NULL) { /* Using server side cursors */ return true; } int xcursor_size = 24; { const char *env_cursor_size = getenv("XCURSOR_SIZE"); if (env_cursor_size != NULL) { errno = 0; char *end; int size = (int)strtol(env_cursor_size, &end, 10); if (errno == 0 && *end == '\0' && size > 0) xcursor_size = size; else LOG_WARN("XCURSOR_SIZE '%s' is invalid, defaulting to 24", env_cursor_size); } } const char *xcursor_theme = getenv("XCURSOR_THEME"); LOG_INFO("cursor theme: %s, size: %d, scale: %.2f", xcursor_theme ? xcursor_theme : "(null)", xcursor_size, new_scale); seat->pointer.theme = wl_cursor_theme_load( xcursor_theme, xcursor_size * new_scale, seat->wayl->shm); if (seat->pointer.theme == NULL) { LOG_ERR("failed to load cursor theme"); return false; } seat->pointer.scale = new_scale; return true; } void wayl_flush(struct wayland *wayl) { while (true) { int r = wl_display_flush(wayl->display); if (r >= 0) { /* Most likely code path - the flush succeed */ return; } if (errno == EINTR) { /* Unlikely */ continue; } if (errno != EAGAIN) { LOG_ERRNO("failed to flush wayland socket"); return; } /* Socket buffer is full - need to wait for it to become writeable again */ xassert(errno == EAGAIN); while (true) { struct pollfd fds[] = {{.fd = wayl->fd, .events = POLLOUT}}; r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1); if (r < 0) { if (errno == EINTR) continue; LOG_ERRNO("failed to poll"); return; } if (fds[0].revents & POLLHUP) return; xassert(fds[0].revents & POLLOUT); break; } } } void wayl_roundtrip(struct wayland *wayl) { wl_display_cancel_read(wayl->display); if (wl_display_roundtrip(wayl->display) < 0) { LOG_ERRNO("failed to roundtrip Wayland display"); return; } /* I suspect the roundtrip above clears the pending queue, and * that prepare_read() will always succeed in the first call. But, * better safe than sorry... */ while (wl_display_prepare_read(wayl->display) != 0) { if (wl_display_dispatch_pending(wayl->display) < 0) { LOG_ERRNO("failed to dispatch pending Wayland events"); return; } } wayl_flush(wayl); } static void surface_scale_explicit_width_height( const struct wl_window *win, const struct wayl_surface *surf, int width, int height, float scale, bool verify) { if (term_fractional_scaling(win->term)) { LOG_DBG("scaling by a factor of %.2f using fractional scaling " "(width=%d, height=%d) ", scale, width, height); if (verify) { if ((int)roundf(scale * (int)roundf(width / scale)) != width) { BUG("width=%d is not valid with scaling factor %.2f (%d != %d)", width, scale, (int)roundf(scale * (int)roundf(width / scale)), width); } if ((int)roundf(scale * (int)roundf(height / scale)) != height) { BUG("height=%d is not valid with scaling factor %.2f (%d != %d)", height, scale, (int)roundf(scale * (int)roundf(height / scale)), height); } } xassert(surf->viewport != NULL); wl_surface_set_buffer_scale(surf->surf, 1); wp_viewport_set_destination( surf->viewport, roundf(width / scale), roundf(height / scale)); } else { const char *mode UNUSED = term_preferred_buffer_scale(win->term) ? "wl_surface.preferred_buffer_scale" : "legacy mode"; LOG_DBG("scaling by a factor of %.2f using %s " "(width=%d, height=%d)" , scale, mode, width, height); xassert(scale == floorf(scale)); const int iscale = (int)floorf(scale); if (verify) { if (width % iscale != 0) { BUG("width=%d is not valid with scaling factor %.2f (%d %% %d != 0)", width, scale, width, iscale); } if (height % iscale != 0) { BUG("height=%d is not valid with scaling factor %.2f (%d %% %d != 0)", height, scale, height, iscale); } } wl_surface_set_buffer_scale(surf->surf, iscale); } } void wayl_surface_scale_explicit_width_height( const struct wl_window *win, const struct wayl_surface *surf, int width, int height, float scale) { surface_scale_explicit_width_height(win, surf, width, height, scale, false); } void wayl_surface_scale(const struct wl_window *win, const struct wayl_surface *surf, const struct buffer *buf, float scale) { surface_scale_explicit_width_height( win, surf, buf->width, buf->height, scale, true); } void wayl_win_scale(struct wl_window *win, const struct buffer *buf) { const struct terminal *term = win->term; const float scale = term->scale; wayl_surface_scale(win, &win->surface, buf, scale); } void wayl_win_alpha_changed(struct wl_window *win) { struct terminal *term = win->term; if (term->colors.alpha == 0xffff) { struct wl_region *region = wl_compositor_create_region( term->wl->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); } static void activation_token_for_urgency_done(const char *token, void *data) { struct wl_window *win = data; struct wayland *wayl = win->term->wl; win->urgency_token_is_pending = false; xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); } bool wayl_win_set_urgent(struct wl_window *win) { if (win->urgency_token_is_pending) { /* We already have a pending token. Don't request another one, * to avoid flooding the Wayland socket */ return true; } bool success = wayl_get_activation_token( win->term->wl, NULL, 0, win, &activation_token_for_urgency_done, win); if (success) { win->urgency_token_is_pending = true; return true; } return false; } bool wayl_win_ring_bell(const struct wl_window *win) { if (win->term->wl->system_bell == NULL) { static bool have_warned = false; if (!have_warned) { LOG_WARN("compositor does not implement the XDG system bell protocol"); have_warned = true; } return false; } xdg_system_bell_v1_ring(win->term->wl->system_bell, win->surface.surf); return true; } bool wayl_win_csd_titlebar_visible(const struct wl_window *win) { return win->csd_mode == CSD_YES && !win->is_fullscreen && !(win->is_maximized && win->term->conf->csd.hide_when_maximized); } bool wayl_win_csd_borders_visible(const struct wl_window *win) { return win->csd_mode == CSD_YES && !win->is_fullscreen && !win->is_maximized; } bool wayl_win_subsurface_new_with_custom_parent( struct wl_window *win, struct wl_surface *parent, struct wayl_sub_surface *surf, bool allow_pointer_input) { struct wayland *wayl = win->term->wl; surf->surface.surf = NULL; surf->surface.viewport = NULL; surf->sub = NULL; struct wl_surface *main_surface = wl_compositor_create_surface(wayl->compositor); if (main_surface == NULL) { LOG_ERR("failed to instantiate surface for sub-surface"); return false; } surf->surface.color_management = NULL; if (win->term->conf->gamma_correct && wayl->color_management.img_description != NULL) { xassert(wayl->color_management.manager != NULL); surf->surface.color_management = wp_color_manager_v1_get_surface( wayl->color_management.manager, main_surface); wp_color_management_surface_v1_set_image_description( surf->surface.color_management, wayl->color_management.img_description, WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); } struct wl_subsurface *sub = wl_subcompositor_get_subsurface( wayl->sub_compositor, main_surface, parent); if (sub == NULL) { LOG_ERR("failed to instantiate sub-surface"); wl_surface_destroy(main_surface); return false; } struct wp_viewport *viewport = NULL; if (wayl->viewporter != NULL) { viewport = wp_viewporter_get_viewport(wayl->viewporter, main_surface); if (viewport == NULL) { LOG_ERR("failed to instantiate viewport for sub-surface"); wl_subsurface_destroy(sub); wl_surface_destroy(main_surface); return false; } } wl_surface_set_user_data(main_surface, win); wl_subsurface_set_sync(sub); /* Disable pointer and touch events */ if (!allow_pointer_input) { struct wl_region *empty = wl_compositor_create_region(wayl->compositor); wl_surface_set_input_region(main_surface, empty); wl_region_destroy(empty); } surf->surface.surf = main_surface; surf->sub = sub; surf->surface.viewport = viewport; return true; } bool wayl_win_subsurface_new(struct wl_window *win, struct wayl_sub_surface *surf, bool allow_pointer_input) { return wayl_win_subsurface_new_with_custom_parent( win, win->surface.surf, surf, allow_pointer_input); } void wayl_win_subsurface_destroy(struct wayl_sub_surface *surf) { if (surf == NULL) return; if (surf->surface.color_management != NULL) { wp_color_management_surface_v1_destroy(surf->surface.color_management); surf->surface.color_management = NULL; } if (surf->surface.viewport != NULL) { wp_viewport_destroy(surf->surface.viewport); surf->surface.viewport = NULL; } if (surf->sub != NULL) { wl_subsurface_destroy(surf->sub); surf->sub = NULL; } if (surf->surface.surf != NULL) { wl_surface_destroy(surf->surface.surf); surf->surface.surf = NULL; } } static void activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token, const char *token) { LOG_DBG("XDG activation token done: %s", token); struct xdg_activation_token_context *ctx = data; struct wl_window *win = ctx->win; ctx->cb(token, ctx->cb_data); tll_foreach(win->xdg_tokens, it) { if (it->item->xdg_token != xdg_token) continue; xassert(win == it->item->win); free(ctx); xdg_activation_token_v1_destroy(xdg_token); tll_remove(win->xdg_tokens, it); return; } BUG("activation token not found in list"); } static const struct xdg_activation_token_v1_listener activation_token_listener = { .done = &activation_token_done, }; bool wayl_get_activation_token( struct wayland *wayl, struct seat *seat, uint32_t serial, struct wl_window *win, void (*cb)(const char *token, void *data), void *cb_data) { if (wayl->xdg_activation == NULL) return false; struct xdg_activation_token_v1 *token = xdg_activation_v1_get_activation_token(wayl->xdg_activation); if (token == NULL) { LOG_ERR("failed to retrieve XDG activation token"); return false; } struct xdg_activation_token_context *ctx = xmalloc(sizeof(*ctx)); *ctx = (struct xdg_activation_token_context){ .win = win, .xdg_token = token, .cb = cb, .cb_data = cb_data, }; tll_push_back(win->xdg_tokens, ctx); if (seat != NULL && serial != 0) xdg_activation_token_v1_set_serial(token, serial, seat->wl_seat); xdg_activation_token_v1_set_surface(token, win->surface.surf); xdg_activation_token_v1_add_listener(token, &activation_token_listener, ctx); xdg_activation_token_v1_commit(token); return true; } void wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token) { if (wayl->xdg_activation == NULL) return; if (token == NULL) return; xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); } foot-1.21.0/wayland.h000066400000000000000000000322121476600145200143570ustar00rootroot00000000000000#pragma once #include #include #include #include #include #include /* Wayland protocols */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cursor-shape.h" #include "fdm.h" /* Forward declarations */ struct terminal; struct buffer; /* Mime-types we support when dealing with data offers (e.g. copy-paste, or DnD) */ enum data_offer_mime_type { DATA_OFFER_MIME_UNSET, DATA_OFFER_MIME_TEXT_PLAIN, DATA_OFFER_MIME_TEXT_UTF8, DATA_OFFER_MIME_URI_LIST, DATA_OFFER_MIME_TEXT_TEXT, DATA_OFFER_MIME_TEXT_STRING, DATA_OFFER_MIME_TEXT_UTF8_STRING, }; enum touch_state { TOUCH_STATE_INHIBITED = -1, TOUCH_STATE_IDLE, TOUCH_STATE_HELD, TOUCH_STATE_DRAGGING, TOUCH_STATE_SCROLLING, }; struct wayl_surface { struct wl_surface *surf; struct wp_viewport *viewport; struct wp_color_management_surface_v1 *color_management; }; struct wayl_sub_surface { struct wayl_surface surface; struct wl_subsurface *sub; }; struct wl_window; struct wl_clipboard { struct wl_window *window; /* For DnD */ struct wl_data_source *data_source; struct wl_data_offer *data_offer; enum data_offer_mime_type mime_type; char *text; uint32_t serial; }; struct wl_primary { struct zwp_primary_selection_source_v1 *data_source; struct zwp_primary_selection_offer_v1 *data_offer; enum data_offer_mime_type mime_type; char *text; uint32_t serial; }; /* Maps a mouse button to its "owning" surface */ struct button_tracker { int button; int surf_kind; /* TODO: this is really an "enum term_surface" */ bool send_to_client; /* Only valid when surface is the main grid surface */ }; struct rect { int x; int y; int width; int height; }; struct seat { struct wayland *wayl; struct wl_seat *wl_seat; uint32_t wl_name; char *name; /* Focused terminals */ struct terminal *kbd_focus; struct terminal *mouse_focus; struct terminal *ime_focus; /* Keyboard state */ struct wl_keyboard *wl_keyboard; struct { uint32_t serial; struct xkb_context *xkb; struct xkb_keymap *xkb_keymap; struct xkb_state *xkb_state; struct xkb_compose_table *xkb_compose_table; struct xkb_compose_state *xkb_compose_state; struct { int fd; bool dont_re_repeat; int32_t delay; int32_t rate; uint32_t key; } repeat; xkb_mod_index_t mod_shift; xkb_mod_index_t mod_alt; xkb_mod_index_t mod_ctrl; xkb_mod_index_t mod_super; xkb_mod_index_t mod_caps; xkb_mod_index_t mod_num; xkb_mod_mask_t legacy_significant; /* Significant modifiers for the legacy keyboard protocol */ xkb_mod_mask_t kitty_significant; /* Significant modifiers for the kitty keyboard protocol */ xkb_keycode_t key_arrow_up; xkb_keycode_t key_arrow_down; /* Enabled modifiers */ bool shift; bool alt; bool ctrl; bool super; } kbd; /* Pointer state */ struct wl_pointer *wl_pointer; struct { uint32_t serial; /* Client-side cursor */ struct wayl_surface surface; struct wl_cursor_theme *theme; struct wl_cursor *cursor; /* Server-side cursor */ struct wp_cursor_shape_device_v1 *shape_device; float scale; bool hidden; enum cursor_shape shape; char *last_custom_xcursor; struct wl_callback *xcursor_callback; bool xcursor_pending; } pointer; /* Touch state */ struct wl_touch *wl_touch; struct { enum touch_state state; uint32_t serial; uint32_t time; struct wl_surface *surface; int surface_kind; int32_t id; } touch; struct { int x; int y; int col; int row; /* Mouse buttons currently being pressed, and their "owning" surfaces */ tll(struct button_tracker) buttons; /* Double- and triple click state */ int count; int last_released_button; struct timespec last_time; /* We used a discrete axis event in the current pointer frame */ double aggregated[2]; double aggregated_120[2]; bool have_discrete; } mouse; /* Clipboard */ struct wl_data_device *data_device; struct zwp_primary_selection_device_v1 *primary_selection_device; struct wl_clipboard clipboard; struct wl_primary primary; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED /* Input Method Editor */ struct zwp_text_input_v3 *wl_text_input; struct { struct { struct rect pending; struct rect sent; } cursor_rect; struct { struct { char *text; int32_t cursor_begin; int32_t cursor_end; } pending; char32_t *text; struct cell *cells; int count; struct { bool hidden; int start; /* Cell index, inclusive */ int end; /* Cell index, exclusive */ } cursor; } preedit; struct { struct { char *text; } pending; } commit; struct { struct { uint32_t before_length; uint32_t after_length; } pending; } surrounding; uint32_t serial; } ime; #endif }; enum csd_surface { CSD_SURF_TITLE, CSD_SURF_LEFT, CSD_SURF_RIGHT, CSD_SURF_TOP, CSD_SURF_BOTTOM, CSD_SURF_MINIMIZE, CSD_SURF_MAXIMIZE, CSD_SURF_CLOSE, CSD_SURF_COUNT, }; struct monitor { struct wayland *wayl; struct wl_output *output; struct zxdg_output_v1 *xdg; uint32_t wl_name; int x; int y; struct { /* Physical size, in mm */ struct { int width; int height; } mm; /* Physical size, in pixels */ struct { int width; int height; } px_real; /* Scaled size, in pixels */ struct { int width; int height; } px_scaled; } dim; struct { /* PPI, based on physical size */ struct { int x; int y; } real; /* PPI, logical, based on scaled size */ struct { int x; int y; } scaled; } ppi; struct { float scaled; float physical; } dpi; int scale; float refresh; enum wl_output_subpixel subpixel; enum wl_output_transform transform; /* From wl_output */ char *make; char *model; /* From xdg_output */ char *name; char *description; float inch; /* e.g. 24" */ bool use_output_release; }; struct wl_url { const struct url *url; struct wayl_sub_surface surf; }; enum csd_mode {CSD_UNKNOWN, CSD_NO, CSD_YES}; typedef void (*activation_token_cb_t)(const char *token, void *data); /* * This context holds data used both in the token::done callback, and * when cleaning up created, by not-yet-done tokens in * wayl_win_destroy(). */ struct xdg_activation_token_context { struct wl_window *win; /* Need for win->xdg_tokens */ struct xdg_activation_token_v1 *xdg_token; /* Used to match token in done() */ activation_token_cb_t cb; /* User provided callback */ void *cb_data; /* Callback user pointer */ }; struct wayland; struct wl_window { struct terminal *term; struct wayl_surface surface; struct xdg_surface *xdg_surface; struct xdg_toplevel *xdg_toplevel; struct wp_fractional_scale_v1 *fractional_scale; tll(struct xdg_activation_token_context *) xdg_tokens; bool urgency_token_is_pending; bool unmapped; float scale; int preferred_buffer_scale; struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; enum csd_mode csd_mode; struct { struct wayl_sub_surface surface[CSD_SURF_COUNT]; struct fcft_font *font; int move_timeout_fd; uint32_t serial; } csd; struct { bool maximize:1; bool minimize:1; } wm_capabilities; struct wayl_sub_surface search; struct wayl_sub_surface scrollback_indicator; struct wayl_sub_surface render_timer; struct wayl_sub_surface overlay; struct wl_callback *frame_callback; tll(const struct monitor *) on_outputs; /* Outputs we're mapped on */ tll(struct wl_url) urls; bool is_configured; bool is_fullscreen; bool is_maximized; bool is_resizing; bool is_tiled_top; bool is_tiled_bottom; bool is_tiled_left; bool is_tiled_right; bool is_tiled; /* At least one of is_tiled_{top,bottom,left,right} is true */ struct { int width; int height; bool is_activated:1; bool is_fullscreen:1; bool is_maximized:1; bool is_resizing:1; bool is_tiled_top:1; bool is_tiled_bottom:1; bool is_tiled_left:1; bool is_tiled_right:1; enum csd_mode csd_mode; } configure; int resize_timeout_fd; }; struct terminal; struct wayland { struct fdm *fdm; struct key_binding_manager *key_binding_manager; int fd; struct wl_display *display; struct wl_registry *registry; struct wl_compositor *compositor; struct wl_subcompositor *sub_compositor; struct wl_shm *shm; struct zxdg_output_manager_v1 *xdg_output_manager; struct xdg_wm_base *shell; struct zxdg_decoration_manager_v1 *xdg_decoration_manager; struct wl_data_device_manager *data_device_manager; struct zwp_primary_selection_device_manager_v1 *primary_selection_device_manager; struct xdg_activation_v1 *xdg_activation; struct wp_viewporter *viewporter; struct wp_fractional_scale_manager_v1 *fractional_scale_manager; struct wp_cursor_shape_manager_v1 *cursor_shape_manager; 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(FOOT_IME_ENABLED) && FOOT_IME_ENABLED struct zwp_text_input_manager_v3 *text_input_manager; #endif tll(struct monitor) monitors; /* All available outputs */ tll(struct seat) seats; tll(struct terminal *) terms; /* WL_SHM >= 2 */ bool use_shm_release; bool shm_have_argb2101010:1; bool shm_have_xrgb2101010:1; bool shm_have_abgr2101010:1; bool shm_have_xbgr2101010:1; }; struct wayland *wayl_init( struct fdm *fdm, struct key_binding_manager *key_binding_manager, bool presentation_timings); void wayl_destroy(struct wayland *wayl); bool wayl_reload_xcursor_theme(struct seat *seat, float new_scale); void wayl_flush(struct wayland *wayl); void wayl_roundtrip(struct wayland *wayl); bool wayl_fractional_scaling(const struct wayland *wayl); void wayl_surface_scale( const struct wl_window *win, const struct wayl_surface *surf, const struct buffer *buf, float scale); void wayl_surface_scale_explicit_width_height( const struct wl_window *win, const struct wayl_surface *surf, int width, int height, float scale); struct wl_window *wayl_win_init(struct terminal *term, const char *token); void wayl_win_destroy(struct wl_window *win); void wayl_win_scale(struct wl_window *win, const struct buffer *buf); void wayl_win_alpha_changed(struct wl_window *win); bool wayl_win_set_urgent(struct wl_window *win); bool wayl_win_ring_bell(const struct wl_window *win); bool wayl_win_csd_titlebar_visible(const struct wl_window *win); bool wayl_win_csd_borders_visible(const struct wl_window *win); bool wayl_win_subsurface_new( struct wl_window *win, struct wayl_sub_surface *surf, bool allow_pointer_input); bool wayl_win_subsurface_new_with_custom_parent( struct wl_window *win, struct wl_surface *parent, struct wayl_sub_surface *surf, bool allow_pointer_input); void wayl_win_subsurface_destroy(struct wayl_sub_surface *surf); bool wayl_get_activation_token( struct wayland *wayl, struct seat *seat, uint32_t serial, struct wl_window *win, activation_token_cb_t cb, void *cb_data); void wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token); foot-1.21.0/xmalloc.c000066400000000000000000000032121476600145200143500ustar00rootroot00000000000000#include #include #include #include "xmalloc.h" #include "debug.h" static void * check_alloc(void *alloc) { if (unlikely(alloc == NULL)) { FATAL_ERROR(__func__, ENOMEM); } return alloc; } void * xmalloc(size_t size) { if (unlikely(size == 0)) { size = 1; } return check_alloc(malloc(size)); } void * xcalloc(size_t nmemb, size_t size) { xassert(size != 0); return check_alloc(calloc(likely(nmemb) ? nmemb : 1, size)); } void * xrealloc(void *ptr, size_t size) { xassert(size != 0); void *alloc = realloc(ptr, size); return check_alloc(alloc); } void * xreallocarray(void *ptr, size_t n, size_t size) { xassert(n != 0 && size != 0); void *alloc = reallocarray(ptr, n, size); return check_alloc(alloc); } char * xstrdup(const char *str) { return check_alloc(strdup(str)); } char * xstrndup(const char *str, size_t n) { return check_alloc(strndup(str, n)); } char32_t * xc32dup(const char32_t *str) { return check_alloc(c32dup(str)); } static VPRINTF(2) int xvasprintf_(char **strp, const char *format, va_list ap) { va_list ap2; va_copy(ap2, ap); int n = vsnprintf(NULL, 0, format, ap2); if (unlikely(n < 0)) { FATAL_ERROR("vsnprintf", EILSEQ); } va_end(ap2); *strp = xmalloc(n + 1); return vsnprintf(*strp, n + 1, format, ap); } char * xvasprintf(const char *format, va_list ap) { char *str; xvasprintf_(&str, format, ap); return str; } char * xasprintf(const char *format, ...) { va_list ap; va_start(ap, format); char *str = xvasprintf(format, ap); va_end(ap); return str; } foot-1.21.0/xmalloc.h000066400000000000000000000024201476600145200143550ustar00rootroot00000000000000#pragma once #include #include #include #include #include #include "char32.h" #include "macros.h" void *xmalloc(size_t size) XMALLOC; void *xcalloc(size_t nmemb, size_t size) XMALLOC; void *xrealloc(void *ptr, size_t size); void *xreallocarray(void *ptr, size_t n, size_t size); char *xstrdup(const char *str) XSTRDUP; char *xstrndup(const char *str, size_t n) XSTRDUP; char *xasprintf(const char *format, ...) PRINTF(1) XMALLOC; char *xvasprintf(const char *format, va_list va) VPRINTF(1) XMALLOC; char32_t *xc32dup(const char32_t *str) XSTRDUP; static inline void * xmemdup(const void *ptr, size_t size) { return memcpy(xmalloc(size), ptr, size); } static inline char * xstrjoin(const char *s1, const char *s2) { size_t n1 = strlen(s1); size_t n2 = strlen(s2); char *joined = xmalloc(n1 + n2 + 1); memcpy(joined, s1, n1); memcpy(joined + n1, s2, n2 + 1); return joined; } static inline char * xstrjoin3(const char *s1, const char *s2, const char *s3) { size_t n1 = strlen(s1); size_t n2 = strlen(s2); size_t n3 = strlen(s3); char *joined = xmalloc(n1 + n2 + n3 + 1); memcpy(joined, s1, n1); memcpy(joined + n1, s2, n2); memcpy(joined + n1 + n2, s3, n3 + 1); return joined; } foot-1.21.0/xsnprintf.c000066400000000000000000000031741476600145200147530ustar00rootroot00000000000000#include "xsnprintf.h" #include #include #include #include "debug.h" #include "macros.h" /* * ISO C doesn't require vsnprintf(3) to set errno on failure, but * POSIX does: * * "If an output error was encountered, these functions shall return * a negative value and set errno to indicate the error." * * The mandated errors of interest are: * * - EILSEQ: A wide-character code does not correspond to a valid character * - EOVERFLOW: The value of n is greater than INT_MAX * - EOVERFLOW: The value to be returned is greater than INT_MAX * * ISO C11 states: * * "The vsnprintf function returns the number of characters that would * have been written had n been sufficiently large, not counting the * terminating null character, or a negative value if an encoding error * occurred. Thus, the null-terminated output has been completely * written if and only if the returned value is nonnegative and less * than n." * * See also: * * - ISO C11 §7.21.6.12p3 * - https://pubs.opengroup.org/onlinepubs/9699919799/functions/vsnprintf.html * - https://pubs.opengroup.org/onlinepubs/9699919799/functions/snprintf.html */ static size_t xvsnprintf(char *restrict buf, size_t n, const char *restrict format, va_list ap) { int len = vsnprintf(buf, n, format, ap); if (unlikely(len < 0 || len >= (int)n)) { FATAL_ERROR(__func__, (len < 0) ? errno : ENOBUFS); } return (size_t)len; } size_t xsnprintf(char *restrict buf, size_t n, const char *restrict format, ...) { va_list ap; va_start(ap, format); size_t len = xvsnprintf(buf, n, format, ap); va_end(ap); return len; } foot-1.21.0/xsnprintf.h000066400000000000000000000002641476600145200147550ustar00rootroot00000000000000#pragma once #include #include #include "macros.h" size_t xsnprintf(char *restrict buf, size_t n, const char *restrict format, ...) PRINTF(3) NONNULL_ARGS;