From d07c2a5cc9eeb566cde80e0bf212ff50606fc7d3 Mon Sep 17 00:00:00 2001 From: entailz Date: Tue, 12 May 2026 23:33:02 -0700 Subject: [PATCH] tabs and tab overview --- .builds/alpine-x64.yml.disabled | 55 + .builds/alpine-x86.yml.disabled | 43 + .builds/freebsd-x64.yml | 49 + .editorconfig | 17 + .gitignore | 7 + .gitmodules | 0 .woodpecker.yaml | 139 + CHANGELOG.md | 3703 ++++++++++ CODE_OF_CONDUCT.md | 83 + INSTALL.md | 476 ++ LICENSE | 21 + README.md | 1 + async.c | 35 + async.h | 24 + base64.c | 172 + base64.h | 8 + box-drawing.c | 3450 +++++++++ box-drawing.h | 7 + char32.c | 432 ++ char32.h | 115 + client-protocol.h | 45 + client.c | 604 ++ commands.c | 115 + commands.h | 6 + completions/bash/foot | 90 + completions/bash/footclient | 82 + completions/fish/foot.fish | 24 + completions/fish/footclient.fish | 20 + completions/meson.build | 9 + completions/zsh/_foot | 41 + completions/zsh/_footclient | 31 + composed.c | 149 + composed.h | 25 + config.c | 4366 +++++++++++ config.h | 523 ++ csi.c | 2234 ++++++ csi.h | 6 + cursor-shape.c | 130 + cursor-shape.h | 30 + dcs.c | 533 ++ dcs.h | 8 + debug.c | 47 + debug.h | 32 + doc/benchmark.md | 81 + doc/foot-ctlseqs.7.scd | 813 +++ doc/foot.1.scd | 735 ++ doc/foot.ini.5.scd | 2166 ++++++ doc/footclient.1.scd | 214 + doc/meson.build | 49 + doc/sixel-tux-foot.png | Bin 0 -> 297553 bytes doc/tux-foot-ok.png | Bin 0 -> 404008 bytes extract.c | 271 + extract.h | 21 + fdm.c | 496 ++ fdm.h | 34 + foot-features.c | 42 + foot-features.h | 13 + foot-server.desktop | 11 + foot-server.service.in | 15 + foot-server.socket | 10 + foot.desktop | 11 + foot.info | 285 + foot.ini | 319 + footclient.desktop | 11 + generate-version.sh | 60 + grid.c | 1676 +++++ grid.h | 138 + hsl.c | 54 + hsl.h | 5 + icons/hicolor/48x48/apps/foot.png | Bin 0 -> 977 bytes icons/hicolor/scalable/apps/foot.svg | 88 + icons/meson.build | 1 + ime.c | 525 ++ ime.h | 19 + input.c | 3871 ++++++++++ input.h | 40 + key-binding.c | 661 ++ key-binding.h | 200 + keymap.h | 414 ++ kitty-keymap.h | 136 + log.c | 231 + log.h | 70 + macros.h | 211 + main.c | 726 ++ meson.build | 454 ++ meson_options.txt | 29 + misc.c | 63 + misc.h | 12 + notes.txt | 6 + notify.c | 765 ++ notify.h | 95 + osc.c | 1735 +++++ osc.h | 7 + pgo/full-current-session.sh | 8 + pgo/full-headless-cage.sh | 14 + pgo/full-headless-sway-inner.sh | 9 + pgo/full-headless-sway.sh | 24 + pgo/full-inner.sh | 32 + pgo/options | 1 + pgo/partial.sh | 29 + pgo/pgo.c | 442 ++ pgo/pgo.sh | 116 + pyproject.toml | 10 + quirks.c | 86 + quirks.h | 23 + reaper.c | 120 + reaper.h | 17 + render.c | 6421 +++++++++++++++++ render.h | 67 + scripts/benchmark.py | 51 + scripts/generate-alt-random-writes.py | 270 + scripts/generate-builtin-terminfo.py | 230 + scripts/generate-emoji-variation-sequences.py | 102 + scripts/srgb.py | 70 + search.c | 2077 ++++++ search.h | 26 + selection.c | 2919 ++++++++ selection.h | 87 + server.c | 690 ++ server.h | 14 + shm-formats.h | 138 + shm.c | 1109 +++ shm.h | 98 + sixel.c | 2228 ++++++ sixel.h | 50 + slave.c | 573 ++ slave.h | 13 + spawn.c | 205 + spawn.h | 16 + stride.h | 9 + subprojects/fcft.wrap | 3 + subprojects/tllist.wrap | 3 + subprojects/wayland-protocols.wrap | 3 + terminal.c | 5363 ++++++++++++++ terminal.h | 1042 +++ tests/meson.build | 8 + tests/test-config.c | 1559 ++++ themes/aeroroot | 34 + themes/alacritty | 57 + themes/apprentice | 25 + themes/ayu-mirage | 26 + themes/catppuccin-frappe | 38 + themes/catppuccin-latte | 41 + themes/catppuccin-macchiato | 38 + themes/catppuccin-mocha | 38 + themes/chiba-dark | 25 + themes/derp | 23 + themes/deus | 29 + themes/dracula | 23 + themes/dracula-iterm | 23 + themes/electrophoretic | 36 + themes/gruvbox | 42 + themes/gruvbox-dark | 22 + themes/gruvbox-light | 25 + themes/hacktober | 22 + themes/iterm | 27 + themes/jetbrains-darcula | 26 + themes/kitty | 22 + themes/material-amber | 41 + themes/material-design | 25 + themes/modus-operandi | 30 + themes/modus-vivendi | 25 + themes/modus-vivendi-tinted | 25 + themes/molokai | 23 + themes/monokai-pro | 22 + themes/moonfly | 29 + themes/neon | 27 + themes/night-owl | 28 + themes/nightfly | 29 + themes/noirblaze | 29 + themes/nord | 42 + themes/nordiq | 24 + themes/nvim | 56 + themes/nvim-dark | 30 + themes/nvim-light | 33 + themes/onedark | 25 + themes/onehalf-dark | 37 + themes/panda | 27 + themes/paper-color | 49 + themes/paper-color-dark | 26 + themes/paper-color-light | 29 + themes/poimandres | 28 + themes/rezza | 38 + themes/rose-pine | 28 + themes/rose-pine-dawn | 32 + themes/rose-pine-moon | 28 + themes/selenized | 48 + themes/selenized-black | 25 + themes/selenized-dark | 25 + themes/selenized-light | 28 + themes/selenized-white | 28 + themes/solarized | 47 + themes/solarized-dark | 28 + themes/solarized-dark-normal-brights | 30 + themes/solarized-light | 26 + themes/solarized-normal-brights | 54 + themes/srcery | 26 + themes/starlight | 24 + themes/tango | 23 + themes/tempus-autumn | 27 + themes/tempus-classic | 27 + themes/tempus-dawn | 31 + themes/tempus-day | 30 + themes/tempus-dusk | 27 + themes/tempus-fugit | 30 + themes/tempus-future | 27 + themes/tempus-night | 27 + themes/tempus-past | 30 + themes/tempus-rift | 27 + themes/tempus-spring | 27 + themes/tempus-summer | 27 + themes/tempus-tempest | 27 + themes/tempus-totus | 30 + themes/tempus-warp | 27 + themes/tempus-winter | 27 + themes/tokyonight-light | 28 + themes/tokyonight-night | 21 + themes/tokyonight-storm | 21 + themes/visibone | 23 + themes/xterm | 22 + themes/zenburn | 25 + tokenize.c | 103 + tokenize.h | 5 + unicode-mode.c | 107 + unicode-mode.h | 11 + unicode/emoji-variation-sequences.txt | 757 ++ uri.c | 271 + uri.h | 11 + url-mode.c | 830 +++ url-mode.h | 28 + user-notification.c | 14 + user-notification.h | 41 + util.h | 67 + utils/meson.build | 1 + utils/xtgettcap.c | 199 + vt.c | 1133 +++ vt.h | 28 + wayland.c | 2858 ++++++++ wayland.h | 598 ++ xkbcommon-vmod.h | 18 + xmalloc.c | 96 + xmalloc.h | 50 + xsnprintf.c | 55 + xsnprintf.h | 7 + 244 files changed, 72046 insertions(+) create mode 100644 .builds/alpine-x64.yml.disabled create mode 100644 .builds/alpine-x86.yml.disabled create mode 100644 .builds/freebsd-x64.yml create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .woodpecker.yaml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 INSTALL.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 async.c create mode 100644 async.h create mode 100644 base64.c create mode 100644 base64.h create mode 100644 box-drawing.c create mode 100644 box-drawing.h create mode 100644 char32.c create mode 100644 char32.h create mode 100644 client-protocol.h create mode 100644 client.c create mode 100644 commands.c create mode 100644 commands.h create mode 100644 completions/bash/foot create mode 100644 completions/bash/footclient create mode 100644 completions/fish/foot.fish create mode 100644 completions/fish/footclient.fish create mode 100644 completions/meson.build create mode 100644 completions/zsh/_foot create mode 100644 completions/zsh/_footclient create mode 100644 composed.c create mode 100644 composed.h create mode 100644 config.c create mode 100644 config.h create mode 100644 csi.c create mode 100644 csi.h create mode 100644 cursor-shape.c create mode 100644 cursor-shape.h create mode 100644 dcs.c create mode 100644 dcs.h create mode 100644 debug.c create mode 100644 debug.h create mode 100644 doc/benchmark.md create mode 100644 doc/foot-ctlseqs.7.scd create mode 100644 doc/foot.1.scd create mode 100644 doc/foot.ini.5.scd create mode 100644 doc/footclient.1.scd create mode 100644 doc/meson.build create mode 100644 doc/sixel-tux-foot.png create mode 100644 doc/tux-foot-ok.png create mode 100644 extract.c create mode 100644 extract.h create mode 100644 fdm.c create mode 100644 fdm.h create mode 100644 foot-features.c create mode 100644 foot-features.h create mode 100644 foot-server.desktop create mode 100644 foot-server.service.in create mode 100644 foot-server.socket create mode 100644 foot.desktop create mode 100644 foot.info create mode 100644 foot.ini create mode 100644 footclient.desktop create mode 100755 generate-version.sh create mode 100644 grid.c create mode 100644 grid.h create mode 100644 hsl.c create mode 100644 hsl.h create mode 100644 icons/hicolor/48x48/apps/foot.png create mode 100644 icons/hicolor/scalable/apps/foot.svg create mode 100644 icons/meson.build create mode 100644 ime.c create mode 100644 ime.h create mode 100644 input.c create mode 100644 input.h create mode 100644 key-binding.c create mode 100644 key-binding.h create mode 100644 keymap.h create mode 100644 kitty-keymap.h create mode 100644 log.c create mode 100644 log.h create mode 100644 macros.h create mode 100644 main.c create mode 100644 meson.build create mode 100644 meson_options.txt create mode 100644 misc.c create mode 100644 misc.h create mode 100644 notes.txt create mode 100644 notify.c create mode 100644 notify.h create mode 100644 osc.c create mode 100644 osc.h create mode 100755 pgo/full-current-session.sh create mode 100755 pgo/full-headless-cage.sh create mode 100755 pgo/full-headless-sway-inner.sh create mode 100755 pgo/full-headless-sway.sh create mode 100755 pgo/full-inner.sh create mode 100644 pgo/options create mode 100755 pgo/partial.sh create mode 100644 pgo/pgo.c create mode 100755 pgo/pgo.sh create mode 100644 pyproject.toml create mode 100644 quirks.c create mode 100644 quirks.h create mode 100644 reaper.c create mode 100644 reaper.h create mode 100644 render.c create mode 100644 render.h create mode 100755 scripts/benchmark.py create mode 100755 scripts/generate-alt-random-writes.py create mode 100755 scripts/generate-builtin-terminfo.py create mode 100644 scripts/generate-emoji-variation-sequences.py create mode 100755 scripts/srgb.py create mode 100644 search.c create mode 100644 search.h create mode 100644 selection.c create mode 100644 selection.h create mode 100644 server.c create mode 100644 server.h create mode 100644 shm-formats.h create mode 100644 shm.c create mode 100644 shm.h create mode 100644 sixel.c create mode 100644 sixel.h create mode 100644 slave.c create mode 100644 slave.h create mode 100644 spawn.c create mode 100644 spawn.h create mode 100644 stride.h create mode 100644 subprojects/fcft.wrap create mode 100644 subprojects/tllist.wrap create mode 100644 subprojects/wayland-protocols.wrap create mode 100644 terminal.c create mode 100644 terminal.h create mode 100644 tests/meson.build create mode 100644 tests/test-config.c create mode 100644 themes/aeroroot create mode 100644 themes/alacritty create mode 100644 themes/apprentice create mode 100644 themes/ayu-mirage create mode 100644 themes/catppuccin-frappe create mode 100644 themes/catppuccin-latte create mode 100644 themes/catppuccin-macchiato create mode 100644 themes/catppuccin-mocha create mode 100644 themes/chiba-dark create mode 100644 themes/derp create mode 100644 themes/deus create mode 100644 themes/dracula create mode 100644 themes/dracula-iterm create mode 100644 themes/electrophoretic create mode 100644 themes/gruvbox create mode 100644 themes/gruvbox-dark create mode 100644 themes/gruvbox-light create mode 100644 themes/hacktober create mode 100644 themes/iterm create mode 100644 themes/jetbrains-darcula create mode 100644 themes/kitty create mode 100644 themes/material-amber create mode 100644 themes/material-design create mode 100644 themes/modus-operandi create mode 100644 themes/modus-vivendi create mode 100644 themes/modus-vivendi-tinted create mode 100644 themes/molokai create mode 100644 themes/monokai-pro create mode 100644 themes/moonfly create mode 100644 themes/neon create mode 100644 themes/night-owl create mode 100644 themes/nightfly create mode 100644 themes/noirblaze create mode 100644 themes/nord create mode 100644 themes/nordiq create mode 100644 themes/nvim create mode 100644 themes/nvim-dark create mode 100644 themes/nvim-light create mode 100644 themes/onedark create mode 100644 themes/onehalf-dark create mode 100644 themes/panda create mode 100644 themes/paper-color create mode 100644 themes/paper-color-dark create mode 100644 themes/paper-color-light create mode 100644 themes/poimandres create mode 100644 themes/rezza create mode 100644 themes/rose-pine create mode 100644 themes/rose-pine-dawn create mode 100644 themes/rose-pine-moon create mode 100644 themes/selenized create mode 100644 themes/selenized-black create mode 100644 themes/selenized-dark create mode 100644 themes/selenized-light create mode 100644 themes/selenized-white create mode 100644 themes/solarized create mode 100644 themes/solarized-dark create mode 100644 themes/solarized-dark-normal-brights create mode 100644 themes/solarized-light create mode 100644 themes/solarized-normal-brights create mode 100644 themes/srcery create mode 100644 themes/starlight create mode 100644 themes/tango create mode 100644 themes/tempus-autumn create mode 100644 themes/tempus-classic create mode 100644 themes/tempus-dawn create mode 100644 themes/tempus-day create mode 100644 themes/tempus-dusk create mode 100644 themes/tempus-fugit create mode 100644 themes/tempus-future create mode 100644 themes/tempus-night create mode 100644 themes/tempus-past create mode 100644 themes/tempus-rift create mode 100644 themes/tempus-spring create mode 100644 themes/tempus-summer create mode 100644 themes/tempus-tempest create mode 100644 themes/tempus-totus create mode 100644 themes/tempus-warp create mode 100644 themes/tempus-winter create mode 100644 themes/tokyonight-light create mode 100644 themes/tokyonight-night create mode 100644 themes/tokyonight-storm create mode 100644 themes/visibone create mode 100644 themes/xterm create mode 100644 themes/zenburn create mode 100644 tokenize.c create mode 100644 tokenize.h create mode 100644 unicode-mode.c create mode 100644 unicode-mode.h create mode 100644 unicode/emoji-variation-sequences.txt create mode 100644 uri.c create mode 100644 uri.h create mode 100644 url-mode.c create mode 100644 url-mode.h create mode 100644 user-notification.c create mode 100644 user-notification.h create mode 100644 util.h create mode 100644 utils/meson.build create mode 100644 utils/xtgettcap.c create mode 100644 vt.c create mode 100644 vt.h create mode 100644 wayland.c create mode 100644 wayland.h create mode 100644 xkbcommon-vmod.h create mode 100644 xmalloc.c create mode 100644 xmalloc.h create mode 100644 xsnprintf.c create mode 100644 xsnprintf.h diff --git a/.builds/alpine-x64.yml.disabled b/.builds/alpine-x64.yml.disabled new file mode 100644 index 0000000..d1336b6 --- /dev/null +++ b/.builds/alpine-x64.yml.disabled @@ -0,0 +1,55 @@ +image: alpine/edge +packages: + - musl-dev + - eudev-libs + - eudev-dev + - linux-headers + - meson + - ninja + - gcc + - scdoc + - wayland-dev + - wayland-protocols + - freetype-dev + - fontconfig-dev + - harfbuzz-dev + - utf8proc-dev + - pixman-dev + - libxkbcommon-dev + - ncurses + - python3 + - py3-pip + - check-dev + - ttf-hack + - font-noto-emoji + +sources: + - https://git.sr.ht/~dnkl/foot + +# triggers: +# - action: email +# condition: failure +# to: + +tasks: + - fcft: | + cd foot/subprojects + git clone https://codeberg.org/dnkl/fcft.git + cd ../.. + - debug: | + mkdir -p bld/debug + meson --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug + ninja -C bld/debug -k0 + meson test -C bld/debug --print-errorlogs + - release: | + mkdir -p bld/release + meson --buildtype=minsize -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release + ninja -C bld/release -k0 + meson test -C bld/release --print-errorlogs + - codespell: | + python3 -m venv codespell-venv + source codespell-venv/bin/activate + pip install codespell + cd foot + ~/.local/bin/codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + deactivate diff --git a/.builds/alpine-x86.yml.disabled b/.builds/alpine-x86.yml.disabled new file mode 100644 index 0000000..6d79022 --- /dev/null +++ b/.builds/alpine-x86.yml.disabled @@ -0,0 +1,43 @@ +image: alpine/edge +arch: x86 +packages: + - musl-dev + - eudev-libs + - eudev-dev + - linux-headers + - meson + - ninja + - gcc + - scdoc + - wayland-dev + - wayland-protocols + - freetype-dev + - fontconfig-dev + - harfbuzz-dev + - utf8proc-dev + - pixman-dev + - libxkbcommon-dev + - ncurses + - check-dev + - ttf-hack + - font-noto-emoji + +sources: + - https://git.sr.ht/~dnkl/foot + +# triggers: +# - action: email +# condition: failure +# to: + +tasks: + - debug: | + mkdir -p bld/debug + meson --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug + ninja -C bld/debug -k0 + meson test -C bld/debug --print-errorlogs + - release: | + mkdir -p bld/release + meson --buildtype=minsize -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release + ninja -C bld/release -k0 + meson test -C bld/release --print-errorlogs diff --git a/.builds/freebsd-x64.yml b/.builds/freebsd-x64.yml new file mode 100644 index 0000000..77775ac --- /dev/null +++ b/.builds/freebsd-x64.yml @@ -0,0 +1,49 @@ +image: freebsd/latest +packages: + - evdev-proto + - libepoll-shim + - meson + - ninja + - pkgconf + - scdoc + - wayland + - wayland-protocols + - freetype2 + - fontconfig + - harfbuzz + - utf8proc + - pixman + - libxkbcommon + - check + - hack-font + - noto-emoji + +sources: + - https://codeberg.org/dnkl/foot.git + +# triggers: +# - action: email +# condition: failure +# to: + +tasks: + - fcft: | + cd foot/subprojects + git clone https://codeberg.org/dnkl/tllist.git + git clone https://codeberg.org/dnkl/fcft.git + cd ../.. + - debug: | + mkdir -p bld/debug + meson setup --buildtype=debug -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/debug + ninja -C bld/debug -k0 + meson test -C bld/debug --print-errorlogs + bld/debug/foot --version + bld/debug/footclient --version + + - release: | + mkdir -p bld/release + meson setup --buildtype=minsize -Db_pgo=generate -Dterminfo=disabled -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true foot bld/release + ninja -C bld/release -k0 + meson test -C bld/release --print-errorlogs + bld/release/foot --version + bld/release/footclient --version diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ef74858 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 +indent_style = space +indent_size = 4 +max_line_length = 70 + +[{meson.build,PKGBUILD}] +indent_size = 2 + +[*.scd] +indent_style = tab +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..21655ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/bld/ +/build/ +/build-pgo/ +/pkg/ +/src/ +/.cache/ +/subprojects/*/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/.woodpecker.yaml b/.woodpecker.yaml new file mode 100644 index 0000000..900251a --- /dev/null +++ b/.woodpecker.yaml @@ -0,0 +1,139 @@ +# -*- yaml -*- + +steps: + - name: pychecks + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + image: alpine:edge + commands: + - apk add openssl + - apk add python3 + - apk add py3-pip + - python3 -m venv venv + - source venv/bin/activate + - python -m pip install --upgrade pip + - pip install codespell + - pip install mypy + - pip install ruff + - codespell + - mypy + - ruff check + - deactivate + + - name: subprojects + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + image: alpine:edge + commands: + - apk add git + - mkdir -p subprojects && cd subprojects + - git clone https://codeberg.org/dnkl/tllist.git + - git clone https://codeberg.org/dnkl/fcft.git + - cd .. + + - name: x64 + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + depends_on: [subprojects] + image: alpine:edge + commands: + - apk update + - apk add musl-dev linux-headers meson ninja gcc clang scdoc ncurses + - apk add libxkbcommon-dev pixman-dev freetype-dev fontconfig-dev harfbuzz-dev utf8proc-dev + - apk add wayland-dev wayland-protocols + - apk add git + - apk add check-dev + - apk add ttf-hack font-noto-emoji + + # Debug + - mkdir -p bld/debug-x64 + - cd bld/debug-x64 + - meson setup --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - ninja -v -k0 + - ninja -v test + - ./foot --version + - ./footclient --version + - cd ../.. + + # Release (gcc) + - mkdir -p bld/release-x64 + - cd bld/release-x64 + - meson setup --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - ninja -v -k0 + - ninja -v test + - ./foot --version + - ./footclient --version + - cd ../.. + + # Release (clang) + - mkdir -p bld/release-x64-clang + - cd bld/release-x64-clang + - CC=clang meson setup --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - ninja -v -k0 + - ninja -v test + - ./foot --version + - ./footclient --version + - cd ../.. + + # no grapheme clustering + - apk del harfbuzz harfbuzz-dev utf8proc utf8proc-dev + - mkdir -p bld/debug + - cd bld/debug + - meson setup --buildtype=debug -Dgrapheme-clustering=disabled -Dfcft:grapheme-shaping=disabled -Dfcft:run-shaping=disabled -Dfcft:test-text-shaping=false ../.. + - ninja -v -k0 + - ninja -v test + - ./foot --version + - ./footclient --version + - cd ../.. + + - name: x86 + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + depends_on: [subprojects] + image: i386/alpine:edge + commands: + - apk update + - apk add musl-dev linux-headers meson ninja gcc clang scdoc ncurses + - apk add libxkbcommon-dev pixman-dev freetype-dev fontconfig-dev harfbuzz-dev utf8proc-dev + - apk add wayland-dev wayland-protocols + - apk add git + - apk add check-dev + - apk add ttf-hack font-noto-emoji + + # Debug + - mkdir -p bld/debug-x86 + - cd bld/debug-x86 + - meson setup --buildtype=debug -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - ninja -v -k0 + - ninja -v test + - ./foot --version + - ./footclient --version + - cd ../.. + + # Release (gcc) + - mkdir -p bld/release-x86 + - cd bld/release-x86 + - meson setup --buildtype=release -Db_pgo=generate -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - ninja -v -k0 + - ninja -v test + - ./foot --version + - ./footclient --version + - cd ../.. + + # Release (clang) + - mkdir -p bld/release-x86-clang + - cd bld/release-x86-clang + - CC=clang meson setup --buildtype=release -Dgrapheme-clustering=enabled -Dfcft:grapheme-shaping=enabled -Dfcft:run-shaping=enabled -Dfcft:test-text-shaping=true ../.. + - ninja -v -k0 + - ninja -v test + - ./foot --version + - ./footclient --version + - cd ../.. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..675e135 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3703 @@ +# Changelog + +* [Unreleased](#unreleased) +* [1.26.1](#1-26-1) +* [1.26.0](#1-26-0) +* [1.25.0](#1-25-0) +* [1.24.0](#1-24-0) +* [1.23.1](#1-23-1) +* [1.23.0](#1-23-0) +* [1.22.3](#1-22-3) +* [1.22.2](#1-22-2) +* [1.22.1](#1-22-1) +* [1.22.0](#1-22-0) +* [1.21.0](#1-21-0) +* [1.20.2](#1-20-2) +* [1.20.1](#1-20-1) +* [1.20.0](#1-20-0) +* [1.19.0](#1-19-0) +* [1.18.1](#1-18-1) +* [1.18.0](#1-18-0) +* [1.17.2](#1-17-2) +* [1.17.1](#1-17-1) +* [1.17.0](#1-17-0) +* [1.16.2](#1-16-2) +* [1.16.1](#1-16-1) +* [1.16.0](#1-16-0) +* [1.15.3](#1-15-3) +* [1.15.2](#1-15-2) +* [1.15.1](#1-15-1) +* [1.15.0](#1-15-0) +* [1.14.0](#1-14-0) +* [1.13.1](#1-13-1) +* [1.13.0](#1-13-0) +* [1.12.1](#1-12-1) +* [1.12.0](#1-12-0) +* [1.11.0](#1-11-0) +* [1.10.3](#1-10-3) +* [1.10.2](#1-10-2) +* [1.10.1](#1-10-1) +* [1.10.0](#1-10-0) +* [1.9.2](#1-9-2) +* [1.9.1](#1-9-1) +* [1.9.0](#1-9-0) +* [1.8.2](#1-8-2) +* [1.8.1](#1-8-1) +* [1.8.0](#1-8-0) +* [1.7.2](#1-7-2) +* [1.7.1](#1-7-1) +* [1.7.0](#1-7-0) +* [1.6.4](#1-6-4) +* [1.6.3](#1-6-3) +* [1.6.2](#1-6-2) +* [1.6.1](#1-6-1) +* [1.6.0](#1-6-0) +* [1.5.4](#1-5-4) +* [1.5.3](#1-5-3) +* [1.5.2](#1-5-2) +* [1.5.1](#1-5-1) +* [1.5.0](#1-5-0) +* [1.4.4](#1-4-4) +* [1.4.3](#1-4-3) +* [1.4.2](#1-4-2) +* [1.4.1](#1-4-1) +* [1.4.0](#1-4-0) +* [1.3.0](#1-3-0) +* [1.2.3](#1-2-3) +* [1.2.2](#1-2-2) +* [1.2.1](#1-2-1) +* [1.2.0](#1-2-0) + + +## Unreleased +### Added + +* `url.style=none|single|double|curly|dotted|dashed` option added, + allowing you to configure how URL underlines are drawn. The default + is `dotted` ([#2302][2302]). + +[2302]: https://codeberg.org/dnkl/foot/issues/2302 + + +### Changed + +* URL underlines are now dotted by default, instead of plain + underlines. This can be changed with the new `url.style` option. + + +### Deprecated +### Removed +### Fixed + +* Other output (key presses, query replies etc) being mixed with paste + data, both interactive pastes and OSC-52 ([#2307][2307]). +* Scrollback search not working correctly when the terminal + application has enabled the kitty keyboard protocol with release + event reporting ([#2316][2316]). +* Keypad escapes in the legacy keyboard protocol ignoring the shift + modifier ([#2324][2324]). + +[2307]: https://codeberg.org/dnkl/foot/issues/2307 +[2316]: https://codeberg.org/dnkl/foot/issues/2316 +[2324]: https://codeberg.org/dnkl/foot/issues/2324 + + +### Security +### Contributors + + +## 1.26.1 + +### Fixed + +* Wrong documented default value for `initial-color-theme` in + `foot.ini(5)` ([#2292][2292]). +* Occasional crashes when closing a window and + `tweak.pre-apply-damage=yes` (the default) ([#2288][2288]). + +[2292]: https://codeberg.org/dnkl/foot/issues/2292 +[2288]: https://codeberg.org/dnkl/foot/issues/2288 + + +### Contributors + +* Roshless +* vlkrs + + +## 1.26.0 + +### Added + +* `toplevel-tag` option (and `--toplevel-tag` command line options to + `foot` and `footclient`), allowing you to set a custom toplevel + tag. The compositor must implement the new `xdg-toplevel-tag-v1` + Wayland protocol ([#2212][2212]). +* `[colors-dark]` section to `foot.ini`. Replaces `[colors]`. +* `[colors-light]` section to `foot.ini`. Replaces `[colors2]`. +* `XTGETTCAP`: added `query-os-name`, returning the OS foot is + compiled for (e.g. _'Linux'_) ([#2209][2209]). +* `pad` option now supports 4-directional padding format: + `LEFTxTOPxRIGHTxBOTTOM` (e.g., `20x10x20x10`). +* `--config=PATH` option is now automatically passed to new + terminals spawned via `spawn-terminal` action ([#2259][2259]). +* Preliminary (untested) support for background blur via the new + `ext-background-effect-v1` protocol. Enable by setting + `colors-{dark,light}.blur=yes`. Foot needs to have been **built** + against `wayland-protocols >= 1.45`, and the compositor **must** + implement the `ext-background-effect-v1` protocol, **and** the + `blur` effect. + +[2212]: https://codeberg.org/dnkl/foot/issues/2212 +[2209]: https://codeberg.org/dnkl/foot/issues/2209 +[2259]: https://codeberg.org/dnkl/foot/pulls/2259 + + +### Changed + +* When enabling _"focus mode"_ (private mode 1004), foot now sends a + focus event immediately, to inform the application what the current + state is ([#2202][2202]). +* Scrollback search is now case sensitive when the search string + contains at least one upper case character. +* Mouse tracking in SGR pixel mode no longer emits negative column/row + pixel values ([#2226][2226]). +* Foot now always uses ARGB SHM surfaces. In earlier versions, XRGB + surfaces were used for opaque surfaces. Unfortunately, several + compositors had issues when foot switched between ARGB and XRGB + surfaces (for example when switching color theme, or toggling + fullscreen). + +[2202]: https://codeberg.org/dnkl/foot/issues/2202 +[2226]: https://codeberg.org/dnkl/foot/issues/2226 + + +### Deprecated + +* `[colors]` section in `foot.ini`. Use `[colors-dark]` instead. +* `[colors2]` section in `foot.ini`. Use `[colors-light]` instead. + + +### Removed + +* `cursor.color` config option (deprecated in 1.23.0). Use + `colors-{dark,light}.cursor` instead. + + +### Fixed + +* Search mode: composing keys not ignored. +* Crash when triple-clicking a soft-wrapped line and there is a quote + character in the last column. +* Crash when reverse-scrolling (terminfo capability `rin`) such that + the current viewport ends up outside the scrollback ([#2232][2232]). +* Regression: visual glitches in rare circumstances. +* Key release events for shortcuts being sent to the client + application (kitty keyboard protocol only) ([#2257][2257]). +* Crash when application emits sixel RA with a height of 0, a width > + 0, and then starts writing sixel data ([#2267][2267]). +* Crash if shutting down terminal instance while a "pre-apply damage" + thread is running ([#2263][2263]). + +[2232]: https://codeberg.org/dnkl/foot/issues/2232 +[2257]: https://codeberg.org/dnkl/foot/issues/2257 +[2267]: https://codeberg.org/dnkl/foot/issues/2267 +[2263]: https://codeberg.org/dnkl/foot/issues/2263 + + +### Contributors + +* Andrei +* Barinderpreet Singh +* c4llv07e +* Johannes Altmanninger +* nariby +* pi66 +* Ronan Pigott +* Stéphane Klein +* valoq +* Whyme Lyu +* Yaakov Selkowitz + + +## 1.25.0 + +### Added + +* Performance increased and input latency decreased on compositors + that do not release SHM buffers immediately ([#2188][2188]). +* `colors{,2}.dim-blend-towards=black|white` option, allowing you to + select towards which color to blend when dimming text. Defaults to + `black` in `[colors]`, and `white` in `[colors2]` ([#2187][2187]). + +[2188]: https://codeberg.org/dnkl/foot/issues/2188 +[2187]: https://codeberg.org/dnkl/foot/issues/2187 + + +### Changed + +* SHM buffer sizes are now rounded up to nearest page size, and their + stride is always an even multiple of 256 bytes (by default, + configurable by setting `tweak.min-stride-alignment`). This allows + compositor to directly import foot's SHM buffers to the GPU, with + e.g. integrated graphics ([#2182][2182]). +* Jump label colors in the modus-operandi theme, for improved + readability. + +[2182]: https://codeberg.org/dnkl/foot/issues/2182 + + +### Fixed + +* URL labels misplaces when URL contains double-width characters + ([#2179][2179]). +* One space too much consumed when copying (or pipe:ing) contents with + tabs ([#2194][2194]) +* Ensure we render a new frame when changing fullscreen state. Before, + this was automatically done if the window was also resized. But, it + is possible for a compositor to change an application's fullscreen + state without resizing the window. + +[2179]: https://codeberg.org/dnkl/foot/issues/2179 +[2194]: https://codeberg.org/dnkl/foot/issues/2194 + + +### Contributors + +* Charalampos Mitrodimas +* Matthias Heyman + + +## 1.24.0 + +### Added + +* The `uppercase-regex-insert` option controls whether an uppercase hint + character will insert the selected text into the prompt in `regex-copy` + or `show-urls-copy` mode. It defaults to `true`. ([#2159][2159]). + +[2159]: https://codeberg.org/dnkl/foot/issues/2159 + +### Changed + +* The label letters are no longer sorted before being assigned to URLs + ([#2140][2140]). +* Sending SIGUSR1/SIGUSR2 to a `foot --server` process now causes + newly spawned client instances to use the selected theme, instead of + the original one. +* SIGUSR1/SIGUSR2 can now be sent to `footclient` processes, to change + the theme of that particular instance ([#2156][2156]). + +[2156]: https://codeberg.org/dnkl/foot/issues/2156 + + +### Fixed + +* Invalid configuration values overriding valid ones in surprising + ways. +* Bug where the libutempter utmp backend did not record logouts + correctly. + +### Contributors + +* Ryan Roden-Corrent +* Tobias Mock + + +## 1.23.1 + +### Changed + +* URL labels are now assigned in reverse order, from bottom to + top. This ensures the **last** URL (which is often the one you are + interested in) is always assigned the same key ([#2140][2140]). +* Sending `SIGUSR1` no longer **toggles** between `[colors]` and + `[colors2]`, but explicitly changes to `[colors]`. `SIGUSR2` changes + to `[colors2]` ([#2144][2144]). + +[2140]: https://codeberg.org/dnkl/foot/issues/2140 +[2144]: https://codeberg.org/dnkl/foot/issues/2144 + + +### Fixed + +* 10-bit surfaces sometimes used instead of 16-bit. +* OSC-104/110/111/112/117/119 (reset colors) not taking the currently + active theme into account. + + +## 1.23.0 + +### Added + +* `colors2` config section. This section duplicates the `colors` + section, and lets you define an alternative color theme. +* `key-bindings.color-theme-switch-1`, + `key-bindings.color-theme-switch-2` and + `key-bindings.color-theme-toggle` key bindings. These can be used to + switch between the primary and alternative color themes. They are + not bound by default. +* Sending `SIGUSR1` to the foot process now triggers a theme switch + (in server mode, **all** instances toggles their themes). +* Support for private mode 2031 - [_Dark and Light Mode + Detection_](https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) + ([#2025][2025]) +* Added `initial-color-theme=1|2` config option. `1` uses colors from + the `[colors]` section, `2` uses `[colors2]`. +* Combined dark/light theme files for (dark variant is the default, + set `initial-color-theme=2` to use the light variant by default): + - gruvbox + - nvim + - paper-color + - selenized + - solarized +* `regex-copy`/`show-urls-copy` will copy and paste the selected text if the hint + is completed with an uppercase character ([#1975][1975]). +* `16-bit` to `tweak.surface-bit-depth`. Makes foot use 16-bit image + buffers. They provide the necessary color precision required by + `gamma-correct-blending=yes`. +* New cursor shapes, from `cursor-shape-v1` version 2. +* `center-when-fullscreen` and `center-when-maximized-and-fullscreen` + to the `pad` option. This allows you to configure when the grid is + centered in more detail ([#2111][2111]). + +[2025]: https://codeberg.org/dnkl/foot/issues/2025 +[1975]: https://codeberg.org/dnkl/foot/issues/1975 +[2111]: https://codeberg.org/dnkl/foot/issues/2111 + + +### Changed + +* `cursor.color` moved to `colors.cursor`. +* OSC-11 without an alpha value will now restore the configured + (i.e. from `foot.ini`) alpha, rather than keeping whatever the + current alpha value is, unchanged. +* `gamma-correct-blending=yes` now defaults to `16-bit` image buffers, + instead of `10-bit`. +* Allow setting either selection background, or selection foreground, + only ([#1846][1846]). +* Drop required version of libxkbcommon from 1.8.0 back to 1.0.0 + ([#2103][2103]). +* OSC-52: an empty payload now clears the clipboard. +* DA (Device Attributes): include `52` in the reply, to indicate + OSC-52 support (when at least _copy_ has been enabled in + `security.osc52`). + +[1846]: https://codeberg.org/dnkl/foot/issues/1846 +[2103]: https://codeberg.org/dnkl/foot/issues/2103 + + +### Deprecated + +* `cursor.color` config option; use `colors.cursor` instead. + + +### Removed + +* Subsurface unmap quirk for Sway. This was a workaround added in + 1.12.1, for Sway issue [#6960][sway-6960]. + + +### Fixed + +* `REP`: wrong width of repeated multi-codepoint graphemes. +* Incorrect surface commit after a configure event, under certain + conditions ([#2105][2105]). + +[2105]: https://codeberg.org/dnkl/foot/issues/2105 + + +### Contributors + +* Chen Mulong +* Kirill Primak +* Ryan Roden-Corrent +* tokyo4j + + +## 1.22.3 + +### Added + +* `auto` to the `tweak.surface-bit-depth` option. + + +### Changed + +* `gamma-correct-blending` now defaults to `no` instead of `yes`. +* `tweak.surface-bit-depth` default value changed to `auto`; uses + 10-bit surfaces when `gamma-correct-blending=yes`, and 8-bit + surfaces otherwise. + + +### Fixed + +* Inaccurate colors when `gamma-correct-blending=yes` ([#2082][2082]). + +[2082]: https://codeberg.org/dnkl/foot/issues/2082 + + +## 1.22.2 + +### Changed + +* `gamma-correct-blending=yes` now uses a pure gamma 2.2 transfer + function, instead of the piece-wise sRGB transfer function, to match + what compositors do. + + +### Fixed + +* Wrong colors when `gamma-correct-blending=yes` (the default when + there is compositor support). Note that some colors will still be + off by a **very** small amount, due to loss of precision when + converting to a linear color space. ([#2035][2035]). + +[2035]: https://codeberg.org/dnkl/foot/issues/2035 + + +## 1.22.1 + +### Fixed + +* `colors.alpha-mode=matching` not working as intended. +* Grapheme shaping was allowed to be "enabled" at runtime, even though + disabled at compile time. This caused mis-rendering of certain + codepoints ([#2039][2039]). +* Keyboard modifiers not being reset on keyboard leave events + ([#2034][2034]). +* Fallback font (and possibly wrong color) being used when a character + was followed by a zero-width grapheme breaking codepoint (for + example, _LEFT-TO-RIGHT MARK_) ([#2049][2049]). +* Regression: alpha applied to inversed text/selections + ([#2073][2073]). + +[2039]: https://codeberg.org/dnkl/foot/issues/2039 +[2034]: https://codeberg.org/dnkl/foot/issues/2034 +[2049]: https://codeberg.org/dnkl/foot/issues/2049 +[2073]: https://codeberg.org/dnkl/foot/issues/2073 + + +### Contributors + +* Jan Palus +* valoq + + +## 1.22.0 + +### Added + +* Support for toplevel edge constraints. When the compositor indicates + the toplevel has edge constraints, foot will not allow the window to + be resized (via CSDs) in the constrained directions. +* Virtual modifiers (e.g. `Alt` instead of `Mod1`, `Super` instead of + `Mod4` etc) in key bindings are now recognized as being virtual, and + are automatically mapped to the corresponding real modifier. This + means you can use e.g. `Alt+b` instead of `Mod1+b`. +* `alpha-mode` option to `foot.ini`. Defaults to `default`. This + config changes how alpha is handled on background colours not set by + the terminal.(e.g. vim) ([#2026](2026)) + +[2026]: https://codeberg.org/dnkl/foot/issues/2026 + + +### Changed + +* UTF-8 error recovery now discards fewer bytes. +* Auto-calculated dimmed and brightened colors (e.g. when custom dim + colors has not configured) is now done by linear RGB interpolation, + rather than converting to HSL and adjusting the luminance + ([#2006][2006]). +* Virtual modifiers in keyboard events from the compositor are now + supported. This works around various issues seen when running foot + under mutter (GNOME) ([#2009][2009]): + - Some key combinations generating the wrong escape sequence in the + kitty keyboard protocol. + - some of foot's default shortcuts not working (mainly those using + `Mod1`) out of the box. +* Default URL regex changed to a much more strict variant + ([#2016][2016]). You can manually set the [old + one](https://codeberg.org/dnkl/foot/src/tag/1.21.0/foot.ini#L72), if + you prefer it over the new regex. +* A tiled window can now be resized in the corners (via CSDs), unless + the compositor has indicated the toplevel has edge constraints. + +[2006]: https://codeberg.org/dnkl/foot/issues/2006 +[2009]: https://codeberg.org/dnkl/foot/issues/2009 +[2016]: https://codeberg.org/dnkl/foot/issues/2016 + + +### Fixed + +* Regression: assertion in `url-mode.c` when activating a second URL + via `show-urls-persistent` ([#2000][2000]). +* Build failure (`srgb.h` not found) when doing a parallel build. +* Regression: reflowing (changing the window size) removing empty + lines ([#2011][2011]). +* `url/regex-copy` not handling double-width characters correctly + ([#2027][2027]). + +[2000]: https://codeberg.org/dnkl/foot/issues/2000 +[2011]: https://codeberg.org/dnkl/foot/issues/2011 +[2027]: https://codeberg.org/dnkl/foot/issues/2027 + + +### Contributors + +* Alex Xu (Hello71) +* datsudo +* Dominique Martinet +* Fazzi +* llyyr +* Łukasz Wojniłowicz +* Sam McCall + + +## 1.21.0 + +### Added + +* Support for the new Wayland protocol `xdg-system-bell-v1` protocol + (added in wayland-protocols 1.38), via the new config option + `bell.system=no|yes` (defaults to `yes`). +* Support for custom regex matching ([#1386][1386], + [#1872][1872]) +* Support for kitty's text-sizing protocol (`w`, width, only), OSC-66. +* `cursor.style` can now be set to `hollow` ([#1965][1965]). +* `search-bindings.delete-to-start` and + `search-bindings.delete-to-end` key bindings, defaulting to + `Control+u` and `Control+k` respectively ([#1972][1972]). +* Gamma-correct font rendering. Requires compositor support + (`wp_color_management_v1`, and specifically, the `ext_linear` + transfer function). Enabled by default when compositor support is + available. Can be explicitly enabled or disabled with + `gamma-correct-blending=no|yes`. + +[1386]: https://codeberg.org/dnkl/foot/issues/1386 +[1872]: https://codeberg.org/dnkl/foot/issues/1872 +[1965]: https://codeberg.org/dnkl/foot/issues/1965 +[1972]: https://codeberg.org/dnkl/foot/issues/1972 + + +### Changed + +* Do not try to set a zero width, or height, if the compositor sends a + _configure_ event with only one dimension being zero + ([#1925][1925]). +* Auto-detection of URLs (i.e. not OSC-8 based URLs) are now regex + based. +* Rename Tokyo Night Day theme to Tokyo Night Light and update colors. +* fcft >= 3.3.1 is now required. + - `tweak.scaling-filter` now supports more scaling-filters + - scaled bitmap fonts (when enabled in FontConfig) no longer have a + scaling-filter applied +* Linefeed:ing control characters (e.g. `\n`) no longer **clears** a + row's internal linebreak flag. This fixes an issue where + e.g. multi-line prompt input in fish is treated as separate lines, + rather than one logical, when selecting and copying it + ([#1487][1487]). +* wayland-protocols >= 1.41 is now required. + +[1925]: https://codeberg.org/dnkl/foot/issues/1925 +[1487]: https://codeberg.org/dnkl/foot/issues/1487 + + +### Removed + +* `url.uri-characters` and `url.protocols`. Both options have been + replaced by `url.regex`. +* `notify` option (has been deprecated since 1.18.0). +* `notify-focus-inhibit` option (has been deprecated since 1.18.0). + + +### Fixed + +* Kitty keyboard protocol: alternate key reporting failing to report + the alternate codepoint in some corner cases ([#1918][1918]). +* `foot` and `footclient` hanging, or terminating with `SIGABRT`, when + starting inside a directory whose total length is more than 1024 + characters. +* Regression: reflowing (resizing the window) a line that ends with a + double-width glyph that was pushed to the next line due to there + being only one cell left on current line, did not remove the virtual + space inserted at the end of the current line. +* Wrong key bindings executed when using alternative keyboard layouts + ([#1929][1929]). +* Foot not closing file descriptors for unrecognized or `no_keymap` + keymaps. +* Combining characters (including emojis consisting of multiple + codepoints) not being handled correctly when _insert mode_ is + enabled ([#1947][1947]). +* Reflow of the cursor (active + saved) when at the end of the line + with a pending wrap (LCF set) ([#1954][1954]). +* ~~Zero-width characters that also are grapheme breaks (e.g. U+200B, + ZERO WIDTH SPACE) being ignored (discarded and never stored in the + grid) ([#1960][1960]).~~ (reverted) +* `--server=` not working on FreeBSD ([#1956][1956]). +* Crash when resetting the terminal and an application had previously + set a custom app ID ([#1963][1963]) +* Grapheme clustering state not reset on cursor movements. +* Kitty keyboard protocol: no release events emitted for composed + keys. +* IME: the initial cursor position was reported as 0,0,0,0 + ([#1994][1994]). + +[1918]: https://codeberg.org/dnkl/foot/issues/1918 +[1929]: https://codeberg.org/dnkl/foot/issues/1929 +[1947]: https://codeberg.org/dnkl/foot/issues/1947 +[1954]: https://codeberg.org/dnkl/foot/issues/1954 +[1960]: https://codeberg.org/dnkl/foot/issues/1960 +[1956]: https://codeberg.org/dnkl/foot/issues/1956 +[1963]: https://codeberg.org/dnkl/foot/issues/1963 +[1994]: https://codeberg.org/dnkl/foot/issues/1994 + + +### Contributors + +* Adrian fxj9a +* Alexander Orzechowski +* Attila Fidan +* camel-cdr +* Craig Barnes +* Guillaume Outters +* Johannes Altmanninger +* Ludovico Gerardi +* sewn +* Thomas Bonnefille + + +## 1.20.2 + +### Changed + +* The `CSI 21 t` (report window title) and `OSC 176 ?` (report app-id) + escape sequences are now ignored ([#1894][1894]). + +[1894]: https://codeberg.org/dnkl/foot/issues/1894 + + +### Fixed + +* 'flash' overlay (triggered by either `tput flash`, or enabling + `bell.visual` and then sending `BEL` to the terminal) stuck when + `colors.flash-alpha=1.0`. +* Crash when compositor sends a keyboard enter event before the foot + window has been mapped ([#1910][1910]). +* Build failures (`utf8proc.h` not found) on at least FreeBSD, but + most likely other BSDs, as well as some Linuxes ([#1903][1903]). + +[1910]: https://codeberg.org/dnkl/foot/issues/1910 +[1903]: https://codeberg.org/dnkl/foot/issues/1903 + + +### Contributors + +* Alexander Orzechowski + + +## 1.20.1 + +### Changed + +* Runtime changes to the app-id (OSC-176) now limits the app-id string + to 2048 characters ([#1897][1897]). +* `colors.flash-alpha` can no longer be set to 1.0 (i.e. fully + opaque). This fixes an issue where the window would be stuck in the + flash state. + +[1897]: https://codeberg.org/dnkl/foot/issues/1897 + + +### Fixed + +* Regression: trying to print a Unicode _"Legacy Computing symbol"_, + in the range U+1FB00 - U+1FB9B would crash foot ([#1901][1901]). + +[1901]: https://codeberg.org/dnkl/foot/issues/1901 + + +## 1.20.0 + +### Added + +* Unicode data files updated to Unicode 16. Foot uses these to + determine which VS-15 and VS-16 sequences are valid, and which are + not. +* Box drawing characters U+1CD00...U+1CDE5 (the _"octants"_ from the + _"Symbols for Legacy Computing Supplement"_ codepoint range, added + in Unicode 16.0). +* `security.osc52` option, allowing you to partially or fully disable + host clipboard access via the OSC-52 escape sequence + ([#1867][1867]). + +[1867]: https://codeberg.org/dnkl/foot/issues/1867 + + +### Changed + +* OSC-9: sequences beginning with `;` are now ignored. These + sequences are ConEmu/Windows Terminal sequences, and not intended to + be notifications. +* Use `utf8proc_charwidth()` instead of `wcwidth()`+`wcswidth()` when + calculating character width, when foot has been built with utf8proc + support ([#1865][1865]). +* Run-time changes to the window title, and the app ID now require the + new value to consist of printable characters only. +* Kitty keyboard protocol: Enter, Tab and Backspace no longer report + _release_ events unless _"Report all keys as escape codes"_ is + enabled ([#1892][1892]). + +[1865]: https://codeberg.org/dnkl/foot/issues/1865 +[1892]: https://codeberg.org/dnkl/foot/issues/1892 + + +### Fixed + +* Crash when receiving an OSC-9 or OSC-777 with an empty notification + body ([#1866][1866]). +* Crash when tripple-clicking on region containing `NUL` characters. + +[1866]: https://codeberg.org/dnkl/foot/issues/1866 + + +### Contributors + +* cy +* Denis Zharikov +* heather7283 +* Jack Wilsdon +* Mark Stosberg + + +## 1.19.0 + +### Added + +* `resize-keep-grid` option, controlling whether the window is resized + (and the grid reflowed) or not when e.g. zooming in/out + ([#1807][1807]). +* `strikeout-thickness` option. +* Implemented the new `xdg-toplevel-icon-v1` protocol. +* Implemented `CSI 21 t`: report window title. +* `colors.sixelNN` option, controlling the default sixel color + palette. + +[1807]: https://codeberg.org/dnkl/foot/issues/1807 + + +### Changed + +* `cursor.unfocused-style` is now effective even when `cursor.style` + is not `block`. +* Activating a notification triggered with OSC-777, or BEL, now + focuses the foot window, if XDG activation tokens are supported by + the compositor, the notification daemon, and the notification helper + used by foot (i.e. `desktop-notifications.command`). This has been + supported for OSC-99 since 1.18.0, and now we also support it for + BEL and OSC-777 ([#1822][1822]). +* Sixel background color (when `P2=0|2`) is now set to the **sixel** + color palette entry #0, instead of using the current ANSI background + color. This is what a real VT340 does. +* The `.desktop` files no longer use the reverse DNS naming scheme, + and their names now match the default app-ids used by foot (`foot` + and `footclient`) ([#1607][1607]). +* `file://` prefix are now stripped from OSC-8 URIs when + activated/opened, **if** the hostname matches the hostname of the + computer foot is running on ([#1840][1840]). + +[1822]: https://codeberg.org/dnkl/foot/issues/1822 +[1607]: https://codeberg.org/dnkl/foot/issues/1607 +[1840]: https://codeberg.org/dnkl/foot/issues/1840 + + +### Fixed + +* Some invalid UTF-8 strings passing the validity check when setting + the window title, triggering a Wayland protocol error which then + caused foot to shutdown. +* "Too large" values for `scrollback.lines` causing an integer + overflow, resulting in either visual glitches, crashes, or both + ([#1828][1828]). +* Crash when trying to set an invalid cursor shape with OSC-22, when + foot uses server-side cursor shapes. +* Occasional visual glitches when selecting text, when foot is running + under a compositor that forces foot to double buffer + (e.g. KDE/KWin) ([#1715][1715]). +* Sixels flickering when foot is running under a compositor that + forces foot to double buffer (e.g. KDE, or Smithay based + compositors) ([#1851][1851]). + +[1828]: https://codeberg.org/dnkl/foot/issues/1828 +[1715]: https://codeberg.org/dnkl/foot/issues/1715 +[1851]: https://codeberg.org/dnkl/foot/issues/1851 + + +### Contributors + +* Andrew J. Hesford +* Craig Barnes +* Oleh Hushchenkov +* tokyo4j + + +## 1.18.1 + +### Added + +* OSC-99: support for the `s` parameter. Supported keywords are + `silent`, `system` and names from the freedesktop sound naming + specification. +* `${muted}` and `${sound-name}` added to the + `desktop-notifications.command` template. + + +### Changed + +* CSD buttons now activate on mouse button **release**, rather than + press ([#1787][1787]). + +[1787]: https://codeberg.org/dnkl/foot/issues/1787 + + +### Fixed + +* Regression: OSC-111 not handling alpha changes correctly, causing + visual glitches ([#1801][1801]). + +[1801]: https://codeberg.org/dnkl/foot/issues/1801 + + +### Contributors + +* Craig Barnes +* Shogo Yamazaki + + +## 1.18.0 + +### Added + +* `cursor.blink-rate` option, allowing you to configure the rate the + cursor blinks with (when `cursor.blink=yes`) ([#1707][1707]); +* Support for `wp_single_pixel_buffer_v1`; certain overlay surfaces + will now utilize the new single-pixel buffer protocol. This mainly + reduces the memory usage, but should also be slightly faster. +* Support for high-res mouse wheel scroll events ([#1738][1738]). +* Styled and colored underlines ([#828][828]). +* Support for SGR 21 (double underline). +* Support for `XTPUSHCOLORS`, `XTPOPCOLORS` and `XTREPORTCOLORS`, + i.e. color palette stack ([#856][856]). +* Log output now respects the [`NO_COLOR`](http://no-color.org/) + environment variable ([#1771][1771]). +* Support for [in-band window resize + notifications](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83), + private mode `2048`. +* Support for OSC-99 [_"Kitty desktop + notifications"_](https://sw.kovidgoyal.net/kitty/desktop-notifications/). +* `desktop-notifications.command` option, replaces `notify`. +* `desktop-notifications.inhibit-when-focused` option, replaces + `notify-focus-inhibit`. +* `${category}`, `${urgency}`, `${expire-time}`, `${replace-id}`, + `${icon}` and `${action-argument}` added to the + `desktop-notifications.command` template. +* `desktop-notifications.command-action-argument` option, defining how + `${action-argument}` (in `desktop-notifications.command`) should be + expanded. +* `desktop-notifications.close` option, defining what to execute when + an application wants to close an existing notification (via an + OSC-99 escape sequence). + +[1707]: https://codeberg.org/dnkl/foot/issues/1707 +[1738]: https://codeberg.org/dnkl/foot/issues/1738 +[828]: https://codeberg.org/dnkl/foot/issues/828 +[856]: https://codeberg.org/dnkl/foot/issues/856 +[1771]: https://codeberg.org/dnkl/foot/issues/1771 + + +### Changed + +* All `XTGETTCAP` capabilities are now in the `tigetstr()` format: + + - parameterized string capabilities were previously "source + encoded", meaning e.g. `\E` where not "decoded" into `\x1b`. + - Control characters were also "source encoded", meaning they were + returned as e.g. "^G" instead of `\x07` ([#1701][1701]). + + In other words, if, after this change, `XTGETTCAP` returns a string + that is different compared to `tigetstr()`, then it is likely a bug + in foot's implementation of `XTGETTCAP`. +* If the cursor foreground and background colors are identical (for + example, when cursor uses inverted colors and the cell's foreground + and background are the same), the cursor will instead be rendered + using the default foreground and background colors, inverted + ([#1761][1761]). +* Mouse wheel events now generate `BTN_WHEEL_BACK` and + `BTN_WHEEL_FORWARD` "button presses", instead of `BTN_BACK` and + `BTN_FORWARD`. The default bindings have been updated, and + `scrollback-up-mouse`, `scrollback-down-mouse`, `font-increase` and + `font-decrease` now use the new button names. + + This change allow users to separate physical mouse buttons that + _also_ generates `BTN_BACK` and `BTN_FORWARD`, from wheel scrolling + ([#1763][1763]). +* Replaced the old catppuccin theme with updated flavored themes + pulled from [catppuccin/foot](https://github.com/catppuccin/foot) +* Mouse selections can now be started inside the margins + ([#1702][1702]). + +[1701]: https://codeberg.org/dnkl/foot/issues/1701 +[1761]: https://codeberg.org/dnkl/foot/issues/1761 +[1763]: https://codeberg.org/dnkl/foot/issues/1763 +[1702]: https://codeberg.org/dnkl/foot/issues/1702 + + +### Deprecated + +* `notify` option; replaced by `desktop-notifications.command`. +* `notify-focus-inhibit` option; replaced by + `desktop-notifications.inhibit-when-focused`. + + +### Fixed + +* Crash when zooming in or out, with `dpi-aware=yes`, and the + monitor's DPI is 0 (this is true for, for example, nested Wayland + sessions, or in virtualized environments). +* No error response for empty `XTGETTCAP` request ([#1694][1694]). +* Unicode-mode in one foot client affecting other clients, in foot + server mode ([#1717][1717]). +* IME interfering in URL-mode ([#1718][1718]). +* OSC-52 reply interleaved with other data sent to the client + ([#1734][1734]). +* XKB compose state being reset when foot receives a new keymap + ([#1744][1744]). +* Regression: alpha changes through OSC-11 sequences not taking effect + until window is resized. +* VS15 being ignored ([#1742][1742]). +* VS16 being ignored for a subset of the valid VS16 sequences + ([#1742][1742]). +* Crash in debug builds, when using OSC-12 to set the cursor color and + foot config has not set any custom cursor colors (i.e. without + OSC-12, inverted fg/bg would be used). +* Wrong color used when drawing the unfocused, hollow cursor. +* Encoding of `BTN_BACK` and `BTN_FORWARD`, when sending a mouse input + escape sequence to the terminal application. + +[1694]: https://codeberg.org/dnkl/foot/issues/1694 +[1717]: https://codeberg.org/dnkl/foot/issues/1717 +[1718]: https://codeberg.org/dnkl/foot/issues/1718 +[1734]: https://codeberg.org/dnkl/foot/issues/1734 +[1744]: https://codeberg.org/dnkl/foot/issues/1744 +[1742]: https://codeberg.org/dnkl/foot/issues/1742 + + +### Contributors + +* abs3nt +* Artturin +* Craig Barnes +* Jan Beich +* Mariusz Bialonczyk +* Nicolas Kolling Ribas + + +## 1.17.2 + +### Changed + +* Notifications with invalid UTF-8 strings are now ignored. + + +### Fixed + +* Crash when changing aspect ratio of a sixel, in the middle of the + sixel data (this is unsupported in foot, but should of course not + result in a crash). +* Crash when printing double-width (or longer) characters to, or near, + the last column, when auto-wrap (private mode 7) has been disabled. +* Dynamically sized sixel being trimmed to nothing. +* Flickering with `dpi-aware=yes` and window is unmapped/remapped + (some compositors do this when window is minimized), in a + multi-monitor setup with different monitor DPIs. + + +## 1.17.1 + +### Added + +* `cursor.unfocused-style=unchanged|hollow|none` to `foot.ini`. The + default is `hollow` ([#1582][1582]). +* New key binding: `quit` ([#1475][1475]). + +[1582]: https://codeberg.org/dnkl/foot/issues/1582 +[1475]: https://codeberg.org/dnkl/foot/issues/1475 + + +### Fixed + +* Log-level not respected by syslog. +* Regression: terminal shutting down when the PTY is closed by the + client application, which may be earlier than when the client + application exits ([#1666][1666]). +* When closing the window, send `SIGHUP` to the client application, + before sending `SIGTERM`. The signal sequence is now `SIGHUP`, wait, + `SIGTERM`, wait `SIGKILL`. +* Crash when receiving a `DECRQSS` request with more than 2 bytes in + the `q` parameter. + +[1666]: https://codeberg.org/dnkl/foot/issues/1666 + + +### Contributors + +* Holger Weiß +* izmyname +* Marcin Puc +* tunjan + + +## 1.17.0 + +### Added + +- Support for opening an existing PTY, e.g. a VM console. + ([#1564][1564]) +* Unicode input mode now accepts input from the numpad as well, + numlock is ignored. +* A new `resize-by-cells` option, enabled by default, allows the size + of floating windows to be constrained to multiples of the cell size. +* Support for custom (i.e. other than ctrl/shift/alt/super) modifiers + in key bindings ([#1348][1348]). +* `pipe-command-output` key binding. +* Support for OSC-176, _"Set App-ID"_ + (https://gist.github.com/delthas/d451e2cc1573bb2364839849c7117239). +* Support for `DECRQM` queries with ANSI/ECMA-48 modes (`CSI Ps $ p`). +* Rectangular edit functions: `DECCARA`, `DECRARA`, `DECCRA`, `DECFRA` + and `DECERA` ([#1633][1633]). +* `Rect` capability to terminfo. +* `fe` and `fd` (focus in/out enable/disable) capabilities to + terminfo. +* `nel` capability to terminfo. + +[1348]: https://codeberg.org/dnkl/foot/issues/1348 +[1633]: https://codeberg.org/dnkl/foot/issues/1633 +[1564]: https://codeberg.org/dnkl/foot/pulls/1564 +[`DECBKM`]: https://vt100.net/docs/vt510-rm/DECBKM.html + + +### Changed + +* config: ARGB color values now default to opaque, rather than + transparent, when the alpha component has been left out + ([#1526][1526]). +* The `foot` process now changes CWD to `/` after spawning the shell + process. This ensures the terminal itself does not "lock" a + directory; for example, preventing a mount point from being + unmounted ([#1528][1528]). +* Kitty keyboard protocol: updated behavior of modifiers bits during + modifier key events, to match the (new [#6913][kitty-6913]) behavior + in kitty >= 0.32.0 ([#1561][1561]). +* When changing font sizes or display scales in floating windows, the + window will be resized as needed to preserve the same grid size. +* `smm` now disables private mode 1036 (_"send ESC when Meta modifies + a key"_), and enables private mode 1034 (_"8-bit Meta mode"_). `rmm` + does the opposite ([#1584][1584]). +* Grid is now always centered in the window, when either fullscreened + or maximized. +* Ctrl+wheel up/down bound to `font-increase` and `font-decrease` + respectively (in addition to the already existing default key + bindings `ctrl-+` and `ctrl+-`). +* Use XRGB pixel format (instead of ARGB) when there is no + transparency. +* Prefer CSS xcursor names, and fallback to legacy X11 names. +* Kitty keyboard protocol: use the `XKB` mode when retrieving locked + modifiers, instead of the `GTK` mode. This fixes an issue where some + key combinations (e.g. Shift+space) produces different results + depending on the state of e.g. the NumLock key. +* Kitty keyboard protocol: filter out **all** locked modifiers (as + reported by XKB), rather than hardcoding it to CapsLock only, when + determining whether a key combination produces text or not. +* CSI-t queries now report pixel values **unscaled**, instead of + **scaled** ([#1643][1643]). +* Sixel: text cursor is now placed on the last text row touched by the + sixel, instead of the text row touched by the _upper_ pixel of the + last sixel ([#chafa-192][chafa-192]). +* Sixel: trailing, fully transparent rows are now trimmed + ([#chafa-192][chafa-192]). +* `1004` (enable focus in/out events) removed from the `XM` terminfo + capability. To enable focus in/out, use the `fe` and `fd` + capabilities instead. +* Tightened the regular expression in the `rv` terminfo capability. +* Tightened the regular expression in the `xr` terminfo capability. +* `DECRQM` queries for private mode 67 ([`DECBKM`]) now reply with mode + value 4 ("permanently reset") instead of 0 ("not recognized"). + +[1526]: https://codeberg.org/dnkl/foot/issues/1526 +[1528]: https://codeberg.org/dnkl/foot/issues/1528 +[1561]: https://codeberg.org/dnkl/foot/issues/1561 +[kitty-6913]: https://github.com/kovidgoyal/kitty/issues/6913 +[1584]: https://codeberg.org/dnkl/foot/issues/1584 +[1643]: https://codeberg.org/dnkl/foot/issues/1643 +[chafa-192]: https://github.com/hpjansson/chafa/issues/192 + + +### Fixed + +* config: improved validation of color values. +* config: double close of file descriptor, resulting in a chain of + errors ultimately leading to a startup failure ([#1531][1531]). +* Crash when using a desktop scaling factor > 1, on compositors that + implements neither the `fractional-scale-v1`, nor the + `cursor-shape-v1` Wayland protocols ([#1573][1573]). +* Crash in `--server` mode when one or more environment variables are + set in `[environment]`. +* Environment variables normally set by foot lost with `footclient + -E,--client-environment` ([#1568][1568]). +* XDG toplevel protocol violation, by trying to set a title that + contains an invalid UTF-8 sequence ([#1552][1552]). +* Crash when erasing the scrollback, when scrollback history is + exactly 0 rows. This happens when `[scrollback].line = 0`, and the + window size (number of rows) is a power of two (i.e. 2, 4, 8, 16 + etc) ([#1610][1610]). +* VS16 (variation selector 16 - emoji representation) should only + affect emojis. +* Pressing a modifier key while the kitty keyboard protocol is enabled + no longer resets the viewport, or clears the selection. +* Crash when failing to load an xcursor image ([#1624][1624]). +* Crash when resizing a dynamically sized sixel (no raster + attributes), with a non-1:1 aspect ratio. +* The default sixel color table is now initialized to the colors used + by the VT340, instead of not being initialized at all (thus + requiring the sixel escape sequence to explicitly set all colors it + used). + +[1531]: https://codeberg.org/dnkl/foot/issues/1531 +[1573]: https://codeberg.org/dnkl/foot/issues/1573 +[1568]: https://codeberg.org/dnkl/foot/issues/1568 +[1552]: https://codeberg.org/dnkl/foot/issues/1552 +[1610]: https://codeberg.org/dnkl/foot/issues/1610 +[1624]: https://codeberg.org/dnkl/foot/issues/1624 + + +### Contributors + +* Alyssa Ross +* Andrew J. Hesford +* Artturin +* Craig Barnes +* delthas +* eugenrh +* Fazzi +* Gregory Anders +* Jan Palus +* Leonardo Hernández Hernández +* LmbMaxim +* Matheus Afonso Martins Moreira +* Sivecano +* Tim Culverhouse +* xnuk + + +## 1.16.2 + +### Fixed + +* Last row and/or column of opaque sixels (not having a size that is a + multiple of the cell size) being the wrong color ([#1520][1520]). + +[1520]: https://codeberg.org/dnkl/foot/issues/1520 + + +## 1.16.1 + +### Fixed + +* Foot not starting on linux kernels before 6.3 ([#1514][1514]). +* Cells underneath erased sixels not being repainted ([#1515][1515]). + +[1514]: https://codeberg.org/dnkl/foot/issues/1514 +[1515]: https://codeberg.org/dnkl/foot/issues/1515 + + +## 1.16.0 + +### Added + +* Support for building with _wayland-protocols_ as a subproject. +* Mouse wheel scrolls can now be used in `mouse-bindings` + ([#1077][1077]). +* New mouse bindings: `scrollback-up-mouse` and + `scrollback-down-mouse`, bound to `BTN_BACK` and `BTN_FORWARD` + respectively. +* New key binding: `select-quote`. This key binding selects text + between quote characters, and falls back to selecting the entire + row ([#1364][1364]). +* Support for DECSET/DECRST/DECRQM 2027 (_Grapheme cluster + processing_). +* New **search mode** key bindings (along with their defaults) + ([#419][419]): + - `extend-char` (shift+right) + - `extend-line-down` (shift+down) + - `extend-backward-char` (shift+left) + - `extend-backward-to-word-boundary` (ctrl+shift+left) + - `extend-backward-to-next-whitespace` (none) + - `extend-line-up` (shift+up) + - `scrollback-up-page` (shift+page-up) + - `scrollback-up-half-page` (none) + - `scrollback-up-line` (none) + - `scrollback-down-page` (shift+page-down) + - `scrollback-down-half-page` (none) + - `scrollback-down-line` (none) +* Support for visual bell which flashes the terminal window. + ([#1337][1337]). + +[1077]: https://codeberg.org/dnkl/foot/issues/1077 +[1364]: https://codeberg.org/dnkl/foot/issues/1364 +[419]: https://codeberg.org/dnkl/foot/issues/419 +[1337]: https://codeberg.org/dnkl/foot/issues/1337 + + +### Changed + +* Minimum required version of _wayland-protocols_ is now 1.32 + ([#1391][1391]). +* `foot-server.service` systemd now checks for + `ConditionEnvironment=WAYLAND_DISPLAY` for consistency with the + socket unit ([#1448][1448]) +* Default key binding for `select-row` is now `BTN_LEFT+4`. However, + in many cases, triple clicking will still be enough to select the + entire row; see the new key binding `select-quote` (mapped to + `BTN_LEFT+3` by default) ([#1364][1364]). +* `file://` prefix from URI's are no longer stripped when + opened/activated ([#1474][1474]). +* `XTGETTCAP` with capabilities that are not properly hex encoded will + be ignored, instead of echo:ed back to the TTY in an error response. +* Command line configuration overrides are now applied even if the + configuration file does not exist or can't be + parsed. ([#1495][1495]). +* Wayland surface damage is now more fine-grained. This should result + in lower latencies in many use cases, especially on high DPI + monitors. + +[1391]: https://codeberg.org/dnkl/foot/issues/1391 +[1448]: https://codeberg.org/dnkl/foot/pulls/1448 +[1474]: https://codeberg.org/dnkl/foot/pulls/1474 +[1495]: https://codeberg.org/dnkl/foot/pulls/1495 + + +### Removed + +* `utempter` config option (was deprecated in 1.15.0). + + +### Fixed + +* Race condition for systemd units start in GNOME and KDE + ([#1436][1436]). +* One frame being rendered at the wrong scale after being hidden by + another opaque, maximized window ([#1464][1464]). +* Double-width characters, and grapheme clusters breaking URL + auto-detection ([#1465][1465]). +* Crash when `XDG_ACTIVATION_TOKEN` is set, but compositor does not + support XDG activation ([#1493][1493]). +* Crash when compositor calls `fractional_scale::preferred_scale()` + when there are no monitors (for example, after a monitor has been + turned off and then back on again) ([#1498][1498]). +* Transparency in margins (padding) not being disabled in fullscreen + mode ([#1503][1503]). +* Crash when a scrollback search match is in the last column. +* Scrollback search: grapheme clusters not matching correctly. +* Wrong baseline offset for some fonts ([#1511][1511]). + +[1436]: https://codeberg.org/dnkl/foot/issues/1436 +[1464]: https://codeberg.org/dnkl/foot/issues/1464 +[1465]: https://codeberg.org/dnkl/foot/issues/1465 +[1493]: https://codeberg.org/dnkl/foot/pulls/1493 +[1498]: https://codeberg.org/dnkl/foot/issues/1498 +[1503]: https://codeberg.org/dnkl/foot/issues/1503 +[1511]: https://codeberg.org/dnkl/foot/issues/1511 + +### Contributors + +* 6t8k +* Alyssa Ross +* CismonX +* Max Gautier +* raggedmyth +* Raimund Sacherer +* Sertonix + + +## 1.15.3 + +### Fixed + +* `-f,--font` command line option not affecting `csd.font` (if unset). +* Vertical alignment in URL jump labels, and the scrollback position + indicator. The fix in 1.15.2 was incorrect, and was reverted in the + last minute. But we forgot to remove the entry from the changelog + ([#1430][1430]). + + +## 1.15.2 + +### Added + +* `[tweak].bold-text-in-bright-amount` option ([#1434][1434]). +* `-Dterminfo-base-name` meson option, allowing you to name the + terminfo files to something other than `-Ddefault-terminfo`. Use + case: have foot default to using the terminfo from ncurses (`foot`, + `foot-direct`), while still packaging foot's terminfo files, but + under a different name (e.g. `foot-extra`, `foot-extra-direct`). + +[1434]: https://codeberg.org/dnkl/foot/issues/1434 + + +### Fixed + +* Crash when copying text that contains invalid UTF-8 ([#1423][1423]). +* Wrong font size after suspending the monitor ([#1431][1431]). +* Vertical alignment in URL jump labels, and the scrollback position + indicator ([#1430][1430]). +* Regression: line- and box drawing characters not covering the full + height of the line, when a custom `line-height` is being used + ([#1430][1430]). +* Crash when compositor does not implement the _viewporter_ interface + ([#1444][1444]). +* CSD rendering with fractional scaling ([#1441][1441]). +* Regression: crash with certain combinations of + `--window-size-chars=NxM` and desktop scaling factors + ([#1446][1446]). + +[1423]: https://codeberg.org/dnkl/foot/issues/1423 +[1431]: https://codeberg.org/dnkl/foot/issues/1431 +[1430]: https://codeberg.org/dnkl/foot/issues/1430 +[1444]: https://codeberg.org/dnkl/foot/issues/1444 +[1441]: https://codeberg.org/dnkl/foot/issues/1441 +[1446]: https://codeberg.org/dnkl/foot/issues/1446 + + +## 1.15.1 + +### Changed + +* When window is mapped, use metadata (DPI, scaling factor, subpixel + configuration) from the monitor we were most recently mapped on, + instead of the one least recently. +* Starlight theme (the default theme) updated to [V4][starlight-v4] +* Background transparency (alpha) is now disabled in fullscreened + windows ([#1416][1416]). +* Foot server systemd units now use the standard + graphical-session.target ([#1281][1281]). +* If `$XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock` does not exist, + `footclient` now tries `$XDG_RUNTIME_DIR/foot.sock`, then + `/tmp/foot.sock`, even if `$WAYLAND_DISPLAY` and/or + `$XDG_RUNTIME_DIR` are defined ([#1281][1281]). +* Font baseline calculation: try to center the text within the line, + instead of anchoring it at the top ([#1302][1302]). + +[starlight-v4]: https://github.com/CosmicToast/starlight/blob/v4/CHANGELOG.md#v4 +[1416]: https://codeberg.org/dnkl/foot/issues/1416 +[1281]: https://codeberg.org/dnkl/foot/pulls/1281 +[1302]: https://codeberg.org/dnkl/foot/issues/1302 + + +### Fixed + +* Use appropriate rounding when applying fractional scales. +* Xcursor not being scaled correctly on `fractional-scale-v1` capable + compositors. +* `dpi-aware=yes` being broken on `fractional-scale-v1` capable + compositors (and when a fractional scaling factor is being used) + ([#1404][1404]). +* Initial font size being wrong on `fractional-scale-v1` capable + compositors, with multiple monitors with different scaling factors + connected ([#1404][1404]). +* Crash when _pointer capability_ is removed from a seat, on + compositors without `cursor-shape-v1 support` ([#1411][1411]). +* Crash on exit, if the mouse is hovering over the foot window (does + not happen on all compositors) +* Visual glitches when CSD titlebar is transparent. + +[1404]: https://codeberg.org/dnkl/foot/issues/1404 +[1411]: https://codeberg.org/dnkl/foot/pulls/1411 + + +### Contributors + +* Ayush Agarwal +* CismonX +* Max Gautier +* Ronan Pigott +* xdavidwu + + +## 1.15.0 + +### Added + +* VT: implemented `XTQMODKEYS` query (`CSI ? Pp m`). +* Meson option `utmp-backend=none|libutempter|ulog|auto`. The default + is `auto`, which will select `libutempter` on Linux, `ulog` on + FreeBSD, and `none` for all others. +* Sixel aspect ratio. +* Support for the new `fractional-scale-v1` Wayland protocol. This + brings true fractional scaling to Wayland in general, and with this + release, to foot. +* Support for the new `cursor-shape-v1` Wayland protocol, i.e. server + side cursor shapes ([#1379][1379]). +* Support for touchscreen input ([#517][517]). +* `csd.double-click-to-maximize` option to `foot.ini`. Defaults to + `yes` ([#1293][1293]). + +[1379]: https://codeberg.org/dnkl/foot/issues/1379 +[517]: https://codeberg.org/dnkl/foot/issues/517 +[1293]: https://codeberg.org/dnkl/foot/issues/1293 + + +### Changed + +* Default color theme is now + [starlight](https://github.com/CosmicToast/starlight) + ([#1321][1321]). +* Minimum required meson version is now 0.59 ([#1371][1371]). +* `Control+Shift+u` is now bound to `unicode-input` instead of + `show-urls-launch`, to follow the convention established in GTK and + Qt ([#1183][1183]). +* `show-urls-launch` now bound to `Control+Shift+o` ([#1183][1183]). +* Kitty keyboard protocol: F3 is now encoded as `CSI 13~` instead of + `CSI R`. The kitty keyboard protocol originally allowed F3 to be + encoded as `CSI R`, but this was removed from the specification + since `CSI R` conflicts with the _"Cursor Position Report"_. +* `[main].utempter` renamed to `[main].utmp-helper`. The old option + name is still recognized, but will log a deprecation warning. +* Meson option `default-utempter-path` renamed to + `utmp-default-helper-path`. +* Opaque sixels now retain the background opacity (when current + background color is the **default** background color) + ([#1360][1360]). +* Text cursor's vertical position after emitting a sixel, when sixel + scrolling is **enabled** (the default) has been updated to match + XTerm's, and the VT382's behavior: the cursor is positioned **on** + the last sixel row, rather than _after_ it. This allows printing + sixels on the last row without scrolling up, but also means + applications may have to explicitly emit a newline to ensure the + sixel is visible. For example, `cat`:ing a sixel in the shell will + typically result in the last row not being visible, unless a newline + is explicitly added. +* Default sixel aspect ratio is now 2:1 instead of 1:1. +* Sixel images are no longer cropped to the last non-transparent row. +* Sixel images are now re-scaled when the font size is changed + ([#1383][1383]). +* `dpi-aware` now defaults to `no`, and the `auto` value has been + removed. +* When using custom cursor colors (`cursor.color` is set in + `foot.ini`), the cursor is no longer inverted when the cell is + selected, or when the cell has the `reverse` (SGR 7) attribute set + ([#1347][1347]). + +[1321]: https://codeberg.org/dnkl/foot/issues/1321 +[1371]: https://codeberg.org/dnkl/foot/pulls/1371 +[1183]: https://codeberg.org/dnkl/foot/issues/1183 +[1360]: https://codeberg.org/dnkl/foot/issues/1360 +[1383]: https://codeberg.org/dnkl/foot/issues/1383 +[1347]: https://codeberg.org/dnkl/foot/issues/1347 + + +### Deprecated + +* `[main].utempter` option. + + +### Removed + +* `auto` value for the `dpi-aware` option. + + +### Fixed + +* Incorrect icon in dock and window switcher on Gnome ([#1317][1317]) +* Crash when scrolling after resizing the window with non-zero + scrolling regions. +* `XTMODKEYS` state not being reset on a terminal reset. +* In Gnome dock foot always groups under "foot client". Change + instances of footclient and foot to appear as "foot client" and + "foot" respectively. ([#1355][1355]). +* Glitchy rendering when alpha (transparency) is changed between + opaque and non-opaque at runtime (using OSC-11). +* Regression: crash when resizing the window when `resize-delay-ms > + 0` ([#1377][1377]). +* Crash when scrolling up while running something that generates a lot + of output (for example, `yes`) ([#1380][1380]). +* Default key binding for URL mode conflicting with Unicode input on + some DEs; `show-urls-launched` is now mapped to `Control+Shift+o` by + default, instead of `Control+Shift+u` ([#1183][1183]). + +[1317]: https://codeberg.org/dnkl/foot/issues/1317 +[1355]: https://codeberg.org/dnkl/foot/issues/1355 +[1377]: https://codeberg.org/dnkl/foot/issues/1377 +[1380]: https://codeberg.org/dnkl/foot/issues/1380 + + +### Contributors + +* Antoine Beaupré +* CismonX +* Craig Barnes +* Dan Bungert +* jdevdevdev +* Kyle Gunger +* locture +* Phillip Susi +* sewn +* ShugarSkull +* Vivian Szczepanski +* Vladimir Bauer +* wout +* CosmicToast + + +## 1.14.0 + +### Added + +* Support for adjusting the thickness of regular underlines ([#1136][1136]). +* Support (optional) for utmp logging with libutempter. +* `kxIN` and `kxOUT` (focus in/out events) to terminfo. +* `name` capability to `XTGETTCAP`. +* String values in `foot.ini` may now be quoted. This can be used to + set a value to the empty string, for example. +* Environment variables can now be **unset**, by setting + `[environment].=""` (quotes are required) ([#1225][1225]). +* `font-size-adjustment=N[px]` option, letting you configure how much + to increment/decrement the font size when zooming in or out + ([#1188][1188]). +* Bracketed paste terminfo entries (`BD`, `BE`, `PE` and `PS`, added + to ncurses in 2022-12-24). Vim makes use of these. +* "Report version" terminfo entries (`XR`/`xr`). +* "Report DA2" terminfo entries (`RV`/`rv`). +* `XF` terminfo capability (focus in/out events available). +* `$TERM_PROGRAM` and `$TERM_PROGRAM_VERSION` environment variables + unset in the slave process. + +[1136]: https://codeberg.org/dnkl/foot/issues/1136 +[1225]: https://codeberg.org/dnkl/foot/issues/1225 +[1188]: https://codeberg.org/dnkl/foot/issues/1188 + + +### Changed + +* Default color theme from a variant of the Zenburn theme, to a + variant of the Solarized dark theme. +* Default `pad` from 2x2 to 0x0 (i.e. no padding at all). +* Current working directory (as set by OSC-7) is now passed to the + program executed by the `pipe-*` key bindings ([#1166][1166]). +* `DECRPM` replies (to `DECRQM` queries) now report a value of `4` + ("permanently reset") instead of `2` ("reset") for DEC private + modes that are known but unsupported. +* Set `PWD` environment variable in the slave process ([#1179][1179]). +* DPI is now forced to 96 when found to be unreasonably high. +* Set default log level to warning ([#1215][1215]). +* Default `grapheme-width-method` from `wcswidth` to `double-width`. +* When determining initial font size, do FontConfig config + substitution if the user-provided font pattern has no {pixel}size + option ([#1287][1287]). +* DECRST of DECCOLM and DECSCLM removed from terminfo. + +[1166]: https://codeberg.org/dnkl/foot/issues/1166 +[1179]: https://codeberg.org/dnkl/foot/issues/1179 +[1215]: https://codeberg.org/dnkl/foot/pulls/1215 +[1287]: https://codeberg.org/dnkl/foot/issues/1287 + + +### Fixed + +* Crash in `foot --server` on key press, after another `footclient` + has terminated very early (for example, by trying to launch a + non-existing shell/client). +* Glitchy rendering when scrolling in the scrollback, on compositors + that does not allow Wayland buffer reuse (e.g. KDE/plasma) + ([#1173][1173]) +* Scrollback search matches not being highlighted correctly, on + compositors that does not allow Wayland buffer reuse + (e.g. KDE/plasma). +* Nanosecs "overflow" when calculating timeout value for + `resize-delay-ms` option. +* Missing backslash in ST terminator in escape sequences in the + built-in terminfo (accessed via XTGETTCAP). +* Crash when interactively resizing the window with a very large + scrollback. +* Crash when a sixel image exceeds the current sixel max height. +* Crash after reverse-scrolling (`CSI Ps T`) in the 'normal' + (non-alternate) screen ([#1190][1190]). +* Background transparency being applied to the text "behind" the + cursor. Only applies to block cursor using inversed fg/bg + colors. ([#1205][1205]). +* Crash when monitor's physical size is "too small" ([#1209][1209]). +* Line-height adjustment when incrementing/decrementing the font size + with a user-set line-height ([#1218][1218]). +* Scaling factor not being correctly applied when converting pt-or-px + config values (e.g. letter offsets, line height etc). +* Selection being stuck visually when `IL` and `DL`. +* URL underlines sometimes still being visible after exiting URL mode. +* Text-bindings, and pipe-* bindings, with multiple key mappings + causing a crash (double-free) on exit ([#1259][1259]). +* Double-width glyphs glitching when surrounded by glyphs overflowing + into the double-width glyph ([#1256][1256]). +* Wayland protocol violation when ack:ing a configure event for an + unmapped surface ([#1249][1249]). +* `xdg_toplevel::set_min_size()` not being called. +* Key bindings with consumed modifiers masking other key bindings + ([#1280][1280]). +* Multi-character compose sequences with the kitty keyboard protocol + ([#1288][1288]). +* Crash when application output scrolls very fast, e.g. `yes` + ([#1305][1305]). +* Crash when application scrolls **many** lines (> ~2³¹). +* DECCOLM erasing the screen ([#1265][1265]). + +[1173]: https://codeberg.org/dnkl/foot/issues/1173 +[1190]: https://codeberg.org/dnkl/foot/issues/1190 +[1205]: https://codeberg.org/dnkl/foot/issues/1205 +[1209]: https://codeberg.org/dnkl/foot/issues/1209 +[1218]: https://codeberg.org/dnkl/foot/issues/1218 +[1259]: https://codeberg.org/dnkl/foot/issues/1259 +[1256]: https://codeberg.org/dnkl/foot/issues/1256 +[1249]: https://codeberg.org/dnkl/foot/issues/1249 +[1280]: https://codeberg.org/dnkl/foot/issues/1280 +[1288]: https://codeberg.org/dnkl/foot/issues/1288 +[1305]: https://codeberg.org/dnkl/foot/issues/1305 +[1265]: https://codeberg.org/dnkl/foot/issues/1265 + + +### Contributors + +* Alexey Sakovets +* Andrea Pappacoda +* Antoine Beaupré +* argosatcore +* Craig Barnes +* EuCaue +* Grigory Kirillov +* Harri Nieminen +* Hugo Osvaldo Barrera +* jaroeichler +* Joakim Nohlgård +* Nick Hastings +* Soren A D +* Torsten Trautwein +* Vladimír Magyar +* woojiq +* Yorick Peterse + + +## 1.13.1 + +### Changed + +* Window is now dimmed while in Unicode input mode. + + +### Fixed + +* Compiling against wayland-protocols < 1.25 +* Crash on buggy compositors (GNOME) that sometimes send pointer-enter + events with a NULL surface. Foot now ignores these events, and the + subsequent motion and leave events. +* Regression: "random" selected empty cells being highlighted as + selected when they should not. +* Crash when either resizing the terminal window, or scrolling in the + scrollback history ([#1074][1074]) +* OSC-8 URLs with matching IDs, but mismatching URIs being incorrectly + connected. + +[1074]: https://codeberg.org/dnkl/foot/pulls/1074 + + +## 1.13.0 + +### Added + +* XDG activation support when opening URLs ([#1058][1058]). +* `-Dsystemd-units-dir=` meson command line option. +* Support for custom environment variables in `foot.ini` + ([#1070][1070]). +* Support for jumping to previous/next prompt (requires shell + integration). By default bound to `ctrl`+`shift`+`z` and + `ctrl`+`shift`+`x` respectively ([#30][30]). +* `colors.search-box-no-match` and `colors.search-box-match` options + to `foot.ini` ([#1112][1112]). +* Very basic Unicode input mode via the new + `key-bindings.unicode-input` and `search-bindings.unicode-input` key + bindings. Note that there is no visual feedback, as the preferred + way of entering Unicode characters is with an IME ([#1116][1116]). +* Support for `xdg_toplevel.wm_capabilities`, to adapt the client-side + decoration buttons to the compositor capabilities ([#1061][1061]). + +[1058]: https://codeberg.org/dnkl/foot/issues/1058 +[1070]: https://codeberg.org/dnkl/foot/issues/1070 +[30]: https://codeberg.org/dnkl/foot/issues/30 +[1112]: https://codeberg.org/dnkl/foot/issues/1112 +[1116]: https://codeberg.org/dnkl/foot/issues/1116 +[1061]: https://codeberg.org/dnkl/foot/pulls/1061 + + +### Changed + +* Use `$HOME` instead of `getpwuid()` to retrieve the user's home + directory when searching for `foot.ini`. +* HT, VT and FF are no longer stripped when pasting in non-bracketed + mode ([#1084][1084]). +* NUL is now stripped when pasting in non-bracketed mode + ([#1084][1084]). +* `alt`+`escape` now emits `\E\E` instead of a `CSI 27` sequence + ([#1105][1105]). + +[1084]: https://codeberg.org/dnkl/foot/issues/1084 +[1105]: https://codeberg.org/dnkl/foot/issues/1105 + + +### Fixed + +* Graphical corruption when viewport is at the top of the scrollback, + and the output is scrolling. +* Improved text reflow of logical lines with trailing empty cells + ([#1055][1055]) +* IME focus is now tracked independently from keyboard focus. +* Workaround for buggy compositors (e.g. some versions of GNOME) + allowing drag-and-drops even though foot has reported it does not + support the offered mime-types ([#1092][1092]). +* Keyboard enter/leave events being ignored if there is no keymap + ([#1097][1097]). +* Crash when application emitted an invalid `CSI 38;5;m`, `CSI + 38:5:m`, `CSI 48;5;m` or `CSI 48:5:m` sequence + ([#1111][1111]). +* Certain dead-key combinations resulting in different escape + sequences compared to kitty, when the kitty keyboard protocol is + used ([#1120][1120]). +* Search matches ending with a double-width character not being + highlighted correctly. +* Selection not being cancelled correctly when scrolled out. +* Extending a multi-page selection behaving inconsistently. +* Poor performance when making very large selections ([#1114][1114]). +* Bogus error message when using systemd socket activation for server + mode ([#1107][1107]) +* Empty line at the bottom after a window resize ([#1108][1108]). + +[1055]: https://codeberg.org/dnkl/foot/issues/1055 +[1092]: https://codeberg.org/dnkl/foot/issues/1092 +[1097]: https://codeberg.org/dnkl/foot/issues/1097 +[1111]: https://codeberg.org/dnkl/foot/issues/1111 +[1120]: https://codeberg.org/dnkl/foot/issues/1120 +[1114]: https://codeberg.org/dnkl/foot/issues/1114 +[1107]: https://codeberg.org/dnkl/foot/issues/1107 +[1108]: https://codeberg.org/dnkl/foot/issues/1108 + + +### Contributors + +* Craig Barnes +* Lorenz +* Max Gautier +* Simon Ser +* Stefan Prosiegel + + +## 1.12.1 + +### Added + +* Workaround for Sway bug [#6960][sway-6960]: scrollback search and + the OSC-555 ("flash") escape sequence leaves dimmed (search) and + yellow (flash) artifacts ([#1046][1046]). +* `Control+Shift+v` and `XF86Paste` have been added to the default set + of key bindings that paste from the clipboard into the scrollback + search buffer. This is in addition to the pre-existing `Control+v` + and `Control+y` bindings. + +[sway-6960]: https://github.com/swaywm/sway/issues/6960 +[1046]: https://codeberg.org/dnkl/foot/issues/1046 + + +### Changed + +* Scrollback search's `extend-to-word-boundary` no longer stops at + space-to-word boundaries, making selection extension feel more + natural. + + +### Fixed + +* build: missing symbols when linking the `pgo` helper binary. +* UI not refreshing when pasting something into the scrollback search + box, that does not result in a grid update (for example, when the + search criteria did not result in any matches) ([#1040][1040]). +* foot freezing in scrollback search mode, using 100% CPU + ([#1036][1036], [#1047][1047]). +* Crash when extending a selection to the next word boundary in + scrollback search mode ([#1036][1036]). +* Scrollback search mode not always highlighting all matches + correctly. +* Sixel options not being reset on hard resets (`\Ec`) + +[1040]: https://codeberg.org/dnkl/foot/issues/1040 +[1036]: https://codeberg.org/dnkl/foot/issues/1036 +[1047]: https://codeberg.org/dnkl/foot/issues/1047 + + +## 1.12.0 + +### Added + +* OSC-22 - set xcursor pointer. +* Add "xterm" as fallback cursor where "text" is not available. +* `[key-bindings].scrollback-home|end` options. +* Socket activation for `foot --server` and accompanying systemd unit + files +* Support for re-mapping input, i.e. mapping input to custom escape + sequences ([#325][325]). +* Support for [DECNKM](https://vt100.net/docs/vt510-rm/DECNKM.html), + which allows setting/saving/restoring/querying the keypad mode. +* Sixel support can be disabled by setting `[tweak].sixel=no` + ([#950][950]). +* footclient: `-E,--client-environment` command line option. When + used, the child process in the new terminal instance inherits the + environment from the footclient process instead of the server's + ([#1004][1004]). +* `[csd].hide-when-maximized=yes|no` option ([#1019][1019]). +* Scrollback search mode now highlights all matches. +* `[key-binding].show-urls-persistent` action. This key binding action + is similar to `show-urls-launch`, but does not automatically exit + URL mode after activating an URL ([#964][964]). +* Support for `CSI > 4 n`, disable _modifyOtherKeys_. Note that since + foot only supports level 1 and 2 (and not level 0), this sequence + does not disable _modifyOtherKeys_ completely, but simply reverts it + back to level 1 (the default). +* `-Dtests=false|true` meson command line option. When disabled, test + binaries will neither be built, nor will `ninja test` attempt to + execute them. Enabled by default ([#919][919]). + +[325]: https://codeberg.org/dnkl/foot/issues/325 +[950]: https://codeberg.org/dnkl/foot/issues/950 +[1004]: https://codeberg.org/dnkl/foot/issues/1004 +[1019]: https://codeberg.org/dnkl/foot/issues/1019 +[964]: https://codeberg.org/dnkl/foot/issues/964 +[919]: https://codeberg.org/dnkl/foot/issues/919 + + +### Changed + +* Minimum required meson version is now 0.58. +* Mouse selections are now finalized when the window is resized + ([#922][922]). +* OSC-4 and OSC-11 replies now uses four digits instead of 2 + ([#971][971]). +* `\r` is no longer translated to `\n` when pasting clipboard data + ([#980][980]). +* Use circles for rendering light arc box-drawing characters + ([#988][988]). +* Example configuration is now installed to + `${sysconfdir}/xdg/foot/foot.ini`, typically resolving to + `/etc/xdg/foot/foot.ini` ([#1001][1001]). + +[922]: https://codeberg.org/dnkl/foot/issues/922 +[971]: https://codeberg.org/dnkl/foot/issues/971 +[980]: https://codeberg.org/dnkl/foot/issues/980 +[988]: https://codeberg.org/dnkl/foot/issues/988 +[1001]: https://codeberg.org/dnkl/foot/issues/1001 + + +### Removed + +* DECSET mode 27127 (which was first added in release 1.6.0). + The kitty keyboard protocol (added in release 1.10.3) can + be used to similar effect. + + +### Fixed + +* Build: missing `wayland_client` dependency in `test-config` + ([#918][918]). +* "(null)" being logged as font-name (for some fonts) when warning + about a non-monospaced primary font. +* Rare crash when the window is resized while a mouse selection is + ongoing ([#922][922]). +* Large selections crossing the scrollback wrap-around ([#924][924]). +* Crash in `pipe-scrollback` ([#926][926]). +* Exit code being 0 when a foot server with no open windows terminate + due to e.g. a Wayland connection failure ([#943][943]). +* Key binding collisions not detected for bindings specified as option + overrides on the command line. +* Crash when seat has no keyboard ([#963][963]). +* Key presses with e.g. `AltGr` triggering key combinations with the + base symbol ([#983][983]). +* Underline cursor sometimes being positioned too low, either making + it look thinner than what it should be, or being completely + invisible ([#1005][1005]). +* Fallback to `/etc/xdg` if `XDG_CONFIG_DIRS` is unset + ([#1008][1008]). +* Improved compatibility with XTerm when `modifyOtherKeys=2` + ([#1009][1009]). +* Window geometry when CSDs are enabled and CSD border width set to a + non-zero value. This fixes window snapping in e.g. GNOME. +* Window size "jumping" when starting an interactive resize when CSDs + are enabled, and CSD border width set to a non-zero value. +* Key binding overrides on the command line having no effect with + `footclient` instances ([#931][931]). +* Search prev/next not updating the selection correctly when the + previous and new match overlaps. +* Various minor fixes to scrollback search, and how it finds the + next/prev match. + +[918]: https://codeberg.org/dnkl/foot/issues/918 +[922]: https://codeberg.org/dnkl/foot/issues/922 +[924]: https://codeberg.org/dnkl/foot/issues/924 +[926]: https://codeberg.org/dnkl/foot/issues/926 +[943]: https://codeberg.org/dnkl/foot/issues/943 +[963]: https://codeberg.org/dnkl/foot/issues/963 +[983]: https://codeberg.org/dnkl/foot/issues/983 +[1005]: https://codeberg.org/dnkl/foot/issues/1005 +[1008]: https://codeberg.org/dnkl/foot/issues/1008 +[1009]: https://codeberg.org/dnkl/foot/issues/1009 +[931]: https://codeberg.org/dnkl/foot/issues/931 + + +### Contributors + +* Ashish SHUKLA +* Craig Barnes +* Enes Hecan +* Johannes Altmanninger +* L3MON4D3 +* Leonardo Neumann +* Mariusz Bialonczyk +* Max Gautier +* Merlin Büge +* jvoisin +* merkix + + +## 1.11.0 + +### Added + +* `[mouse-bindings].selection-override-modifiers` option, specifying + which modifiers to hold to override mouse grabs by client + applications and force selection instead. +* _irc://_ and _ircs://_ to the default set of protocols recognized + when auto-detecting URLs. +* [SGR-Pixels (1016) mouse extended coordinates](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates) is now supported + ([#762](https://codeberg.org/dnkl/foot/issues/762)). +* `XTGETTCAP` - builtin terminfo. See + [README.md::XTGETTCAP](README.md#xtgettcap) for details + ([#846](https://codeberg.org/dnkl/foot/issues/846)). +* `DECRQSS` - _Request Selection or Setting_ + ([#798](https://codeberg.org/dnkl/foot/issues/798)). Implemented settings + are: + - `DECSTBM` - _Set Top and Bottom Margins_ + - `SGR` - _Set Graphic Rendition_ + - `DECSCUSR` - _Set Cursor Style_ +* Support for searching for the last searched-for string in scrollback + search (search for next/prev match with an empty search string). + + +### Changed + +* PaperColorDark and PaperColorLight themes renamed to + paper-color-dark and paper-color-light, for consistency with other + theme names. +* `[scrollback].multiplier` is now applied in "alternate scroll" mode, + where scroll events are translated to fake arrow key presses on the + alt screen ([#859](https://codeberg.org/dnkl/foot/issues/859)). +* The width of the block cursor's outline in an unfocused window is + now scaled by the output scaling factor ("desktop + scaling"). Previously, it was always 1px. +* Foot will now try to change the locale to either "C.UTF-8" or + "en_US.UTF-8" if started with a non-UTF8 locale. If this fails, foot + will start, but only to display a window with an error (user's shell + is not executed). +* `gettimeofday()` has been replaced with `clock_gettime()`, due to it being + marked as obsolete by POSIX. +* `alt+tab` now emits `ESC \t` instead of `CSI 27;3;9~` + ([#900](https://codeberg.org/dnkl/foot/issues/900)). +* File pasted, or dropped, on the alt screen is no longer quoted + ([#379](https://codeberg.org/dnkl/foot/issues/379)). +* Line-based selections now include a trailing newline when copied + ([#869](https://codeberg.org/dnkl/foot/issues/869)). +* Foot now clears the signal mask and resets all signal handlers to + their default handlers at startup + ([#854](https://codeberg.org/dnkl/foot/issues/854)). +* `Copy` and `Paste` keycodes are supported by default for the + clipboard. These are useful for keyboards with custom firmware like + QMK to enable global copy/paste shortcuts that work inside and + outside the terminal (https://codeberg.org/dnkl/foot/pulls/894). + + +### Removed + +* Workaround for slow resize in Sway <= 1.5, when a foot window was + hidden, for example, in a tabbed view + (https://codeberg.org/dnkl/foot/pulls/507). + + +### Fixed + +* Font size adjustment ("zooming") when font is configured with a + **pixelsize**, and `dpi-aware=no` + ([#842](https://codeberg.org/dnkl/foot/issues/842)). +* Key presses triggering keyboard layout switches also emitting CSI + codes in the Kitty keyboard protocol. +* Assertion in `shm.c:buffer_release()` + ([#844](https://codeberg.org/dnkl/foot/issues/844)). +* Crash when setting a key- or mouse binding to the empty string + ([#851](https://codeberg.org/dnkl/foot/issues/851)). +* Crash when maximizing the window and `[csd].size=1` + ([#857](https://codeberg.org/dnkl/foot/issues/857)). +* OSC-8 URIs not getting overwritten (erased) by double-width + characters (e.g. emojis). +* Rendering of CSD borders when `csd.border-width > 0` and desktop + scaling has been enabled. +* Failure to launch when `exec(3)`:ed with an empty argv. +* Pasting from the primary clipboard (mouse middle clicking) did not + reset the scrollback view to the bottom. +* Wrong mouse binding triggered when doing two mouse selections in + very quick (< 300ms) succession + ([#883](https://codeberg.org/dnkl/foot/issues/883)). +* Bash completion giving an error when completing a list of short + options +* Sixel: large image resizes (triggered by e.g. large repeat counts in + `DECGRI`) are now truncated instead of ignored. +* Sixel: a repeat count of 0 in `DECGRI` now emits a single sixel. +* LIGHT ARC box drawing characters incorrectly rendered + platforms ([#914](https://codeberg.org/dnkl/foot/issues/914)). + + +### Contributors + +* [lamonte](https://codeberg.org/lamonte) +* Érico Nogueira +* feeptr +* Felix Lechner +* grtcdr +* Mark Stosberg +* Nicolai Dagestad +* Oğuz Ersen +* Pranjal Kole +* Simon Ser + + +## 1.10.3 + +### Added + +* Kitty keyboard protocol ([#319](https://codeberg.org/dnkl/foot/issues/319)): + - [Report event types](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-events) + (mode `0b10`) + - [Report alternate keys](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-alternates) + (mode `0b100`) + - [Report all keys as escape codes](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-all-keys) + (mode `0b1000`) + - [Report associated text](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#report-text) + (mode `0b10000`) + + +### Fixed + +* Crash when bitmap fonts are scaled down to very small font sizes + ([#830](https://codeberg.org/dnkl/foot/issues/830)). +* Crash when overwriting/erasing an OSC-8 URL. + + +## 1.10.2 + +### Added + +* New value, `max`, for `[tweak].grapheme-width-method`. +* Initial support for the [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/). + Modes supported: + - [Disambiguate escape codes](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#disambiguate) (mode `0b1`) +* "Window menu" (compositor provided) on right clicks on the CSD title + bar. + + +### Fixed + +* An ongoing mouse selection is now finalized on a pointer leave event + (for example by switching workspace while doing a mouse selection). +* OSC-8 URIs in the last column +* OSC-8 URIs sometimes being applied to too many, and seemingly + unrelated cells ([#816](https://codeberg.org/dnkl/foot/issues/816)). +* OSC-8 URIs incorrectly being dropped when resizing the terminal + window with the alternate screen active. +* CSD border not being dimmed when window is not focused. +* Visual corruption with large CSD borders + ([#823](https://codeberg.org/dnkl/foot/issues/823)). +* Mouse cursor shape sometimes not being updated correctly. +* Color palette changes (via OSC 4/104) no longer affect RGB colors + ([#678](https://codeberg.org/dnkl/foot/issues/678)). + + +### Contributors + +* Jonas Ådahl + + +## 1.10.1 + +### Added + +* `-Dthemes=false|true` meson command line option. When disabled, + example theme files are **not** installed. +* XDG desktop file for footclient. + + +### Fixed + +* Regression: `letter-spacing` resulting in a "not a valid option" + error ([#795](https://codeberg.org/dnkl/foot/issues/795)). +* Regression: bad section name in configuration error messages. +* Regression: `pipe-*` key bindings not being parsed correctly, + resulting in invalid error messages + ([#809](https://codeberg.org/dnkl/foot/issues/809)). +* OSC-8 data not being cleared when cell is overwritten + ([#804](https://codeberg.org/dnkl/foot/issues/804), + [#801](https://codeberg.org/dnkl/foot/issues/801)). + + +### Contributors + +* Arnavion +* Craig Barnes +* Soc Virnyl Silab Estela +* Xiretza + + +## 1.10.0 + +### Added + +* `notify-focus-inhibit` boolean option, which can be used to control + whether desktop notifications should be inhibited when the terminal + has keyboard focus +* `[colors].scrollback-indicator` color-pair option, which specifies + foreground and background colors for the scrollback indicator. +* `[key-bindings].noop` action. Key combinations assigned to this + action will not be sent to the application + ([#765](https://codeberg.org/dnkl/foot/issues/765)). +* Color schemes are now installed to `${datadir}/foot/themes`. +* `[csd].border-width` and `[csd].border-color`, allowing you to + configure the width and color of the CSD border. +* Support for `XTMODKEYS` with `Pp=4` and `Pv=2` (_modifyOtherKeys=2_). +* `[colors].dim0-7` options, allowing you to configure custom "dim" + colors ([#776](https://codeberg.org/dnkl/foot/issues/776)). + + +### Changed + +* `[tweak].grapheme-shaping` is now enabled by default when both foot + itself, and fcft has been compiled with support for it. +* Default value of `[tweak].grapheme-width-method` changed from + `double-width` to `wcswidth`. +* INSTALL.md: `--override tweak.grapheme-shaping=no` added to PGO + command line. +* Foot now terminates if there are no available seats - for example, + due to the compositor not implementing a recent enough version of + the `wl_seat` interface ([#779](https://codeberg.org/dnkl/foot/issues/779)). +* Boolean options in `foot.ini` are now limited to + "yes|true|on|1|no|false|off|0", Previously, anything that did not + match "yes|true|on", or a number greater than 0, was treated as + "false". +* `[scrollback].multiplier` is no longer applied when the alternate + screen is in use ([#787](https://codeberg.org/dnkl/foot/issues/787)). + + +### Removed + +* The bundled PKGBUILD. +* Deprecated `bell` option (replaced with `[bell]` section in 1.8.0). +* Deprecated `url-launch`, `jump-label-letters` and `osc8-underline` + options (moved to a dedicated `[url]` section in 1.8.0) + + +### Fixed + +* 'Sticky' modifiers in input handling; when determining modifier + state, foot was looking at **depressed** modifiers, not + **effective** modifiers, like it should. +* Fix crashes after enabling CSD at runtime when `csd.size` is 0. +* Convert `\r` to `\n` when reading clipboard data + ([#752](https://codeberg.org/dnkl/foot/issues/752)). +* Clipboard occasionally ceasing to work, until window has been + re-focused ([#753](https://codeberg.org/dnkl/foot/issues/753)). +* Don't propagate window title updates to the Wayland compositor + unless the new title is different from the old title. + + +### Contributors + +* armin +* Craig Barnes +* Daniel Martí +* feeptr +* Mitja Horvat +* Ronan Pigott +* Stanislav Ochotnický + + +## 1.9.2 + +### Changed + +* PGO helper scripts no longer set `LC_CTYPE=en_US.UTF-8`. But, note + that "full" PGO builds still **require** a UTF-8 locale; you need + to set one manually in your build script + ([#728](https://codeberg.org/dnkl/foot/issues/728)). + + +## 1.9.1 + +### Added + +* Warn when it appears the primary font is not monospaced. Can be + disabled by setting `[tweak].font-monospace-warn=no` + ([#704](https://codeberg.org/dnkl/foot/issues/704)). +* PGO build scripts, in the `pgo` directory. See INSTALL.md - + _Performance optimized, PGO_, for details + ([#701](https://codeberg.org/dnkl/foot/issues/701)). +* Braille characters (U+2800 - U+28FF) are now rendered by foot + itself ([#702](https://codeberg.org/dnkl/foot/issues/702)). +* `-e` command-line option. This option is simply ignored, to appease + program launchers that blindly pass `-e` to any terminal emulator + ([#184](https://codeberg.org/dnkl/foot/issues/184)). + + +### Changed + +* `-Ddefault-terminfo` is now also applied to the generated terminfo + definitions when `-Dterminfo=enabled`. +* `-Dcustom-terminfo-install-location` no longer accepts `no` as a + special value, to disable exporting `TERMINFO`. To achieve the same + result, simply don't set it at all. If it _is_ set, `TERMINFO` is + still exported, like before. +* The default install location for the terminfo definitions have been + changed back to `${datadir}/terminfo`. +* `dpi-aware=auto`: fonts are now scaled using the monitor's DPI only + when **all** monitors have a scaling factor of one + ([#714](https://codeberg.org/dnkl/foot/issues/714)). +* fcft >= 3.0.0 in now required. + + +### Fixed + +* Added workaround for GNOME bug where multiple button press events + (for the same button) is sent to the CSDs without any release or + leave events in between ([#709](https://codeberg.org/dnkl/foot/issues/709)). +* Line-wise selection not taking soft line-wrapping into account + ([#726](https://codeberg.org/dnkl/foot/issues/726)). + + +### Contributors + +* [craigbarnes](https://codeberg.org/craigbarnes) +* Arnavion + + +## 1.9.0 + +### Added + +* Window title in the CSDs + ([#638](https://codeberg.org/dnkl/foot/issues/638)). +* `-Ddocs=disabled|enabled|auto` meson command line option. +* Support for `~`-expansion in the `include` directive + ([#659](https://codeberg.org/dnkl/foot/issues/659)). +* Unicode 13 characters U+1FB3C - U+1FB6F, U+1FB9A and U+1FB9B to list + of box drawing characters rendered by foot itself (rather than using + font glyphs) ([#474](https://codeberg.org/dnkl/foot/issues/474)). +* `XM`+`xm` to terminfo. +* Mouse buttons 6/7 (mouse wheel left/right). +* `url.uri-characters` option to `foot.ini` + ([#654](https://codeberg.org/dnkl/foot/issues/654)). + + +### Changed + +* Terminfo files can now co-exist with the foot terminfo files from + ncurses. See `INSTALL.md` for more information + ([#671](https://codeberg.org/dnkl/foot/issues/671)). +* `bold-text-in-bright=palette-based` now only brightens colors from palette +* Raised grace period between closing the PTY and sending `SIGKILL` (when + terminating the client application) from 4 to 60 seconds. +* When terminating the client application, foot now sends `SIGTERM` immediately + after closing the PTY, instead of waiting 2 seconds. +* Foot now sends `SIGTERM`/`SIGKILL` to the client application's process group, + instead of just to the client application's process. +* `kmous` terminfo capability from `\E[M` to `\E[<`. +* pt-or-px values (`letter-spacing`, etc) and the line thickness + (`tweak.box-drawing-base-thickness`) in box drawing characters are + now translated to pixel values using the monitor's scaling factor + when `dpi-aware=no`, or `dpi-aware=auto` and the scaling factor is + larger than 1 ([#680](https://codeberg.org/dnkl/foot/issues/680)). +* Spawning a new terminal with a working directory that does not exist + is no longer a fatal error. + + +### Removed + +* `km`/`smm`/`rmm` from terminfo; foot prefixes Alt-key combinations + with `ESC`, and not by setting the 8:th "meta" bit, regardless of + `smm`/`rmm`. While this _can_ be disabled by, resetting private mode + 1036, the terminfo should reflect the **default** behavior + ([#670](https://codeberg.org/dnkl/foot/issues/670)). +* Keypad application mode keys from terminfo; enabling the keypad + application mode is not enough to make foot emit these sequences - + you also need to disable private mode 1035 + ([#670](https://codeberg.org/dnkl/foot/issues/670)). + + +### Fixed + +* Rendering into the right margin area with `tweak.overflowing-glyphs` + enabled. +* PGO builds with clang ([#642](https://codeberg.org/dnkl/foot/issues/642)). +* Crash in scrollback search mode when selection has been canceled due + to terminal content updates + ([#644](https://codeberg.org/dnkl/foot/issues/644)). +* Foot process not terminating when the Wayland connection is broken + ([#651](https://codeberg.org/dnkl/foot/issues/651)). +* Output scale being zero on compositors that does not advertise a + scaling factor. +* Slow-to-terminate client applications causing other footclient instances to + freeze when closing a footclient window. +* Underlying cell content showing through in the left-most column of + sixels. +* `cursor.blink` not working in GNOME + ([#686](https://codeberg.org/dnkl/foot/issues/686)). +* Blinking cursor stops blinking, or becoming invisible, when + switching focus from, and then back to a terminal window on GNOME + ([#686](https://codeberg.org/dnkl/foot/issues/686)). + + +### Contributors + +* Nihal Jere +* [nowrep](https://codeberg.org/nowrep) +* [clktmr](https://codeberg.org/clktmr) + + +## 1.8.2 + +### Added + +* `locked-title=no|yes` to `foot.ini` + ([#386](https://codeberg.org/dnkl/foot/issues/386)). +* `tweak.overflowing-glyphs` option, which can be enabled to fix rendering + issues with glyphs of any width that appear cut-off + ([#592](https://codeberg.org/dnkl/foot/issues/592)). + + +### Changed + +* Non-empty lines are now considered to have a hard linebreak, + _unless_ an actual word-wrap is inserted. +* Setting `DECSDM` now _disables_ sixel scrolling, while resetting it + _enables_ scrolling ([#631](https://codeberg.org/dnkl/foot/issues/631)). + + +### Removed + +* The `tweak.allow-overflowing-double-width-glyphs` and + `tweak.pua-double-width` options (which have been superseded by + `tweak.overflowing-glyphs`). + + +### Fixed + +* FD exhaustion when repeatedly entering/exiting URL mode with many + URLs. +* Double free of URL while removing duplicated and/or overlapping URLs + in URL mode ([#627](https://codeberg.org/dnkl/foot/issues/627)). +* Crash when an unclosed OSC-8 URL ran into un-allocated scrollback + rows. +* Some box-drawing characters were rendered incorrectly on big-endian + architectures. +* Crash when resizing the window to the smallest possible size while + scrollback search is active. +* Scrollback indicator being incorrectly rendered when window size is + very small. +* Reduced memory usage in URL mode. +* Crash when the `E3` escape (`\E[3J`) was executed, and there was a + selection, or sixel image, in the scrollback + ([#633](https://codeberg.org/dnkl/foot/issues/633)). + + +### Contributors + +* [clktmr](https://codeberg.org/clktmr) + + +## 1.8.1 + +### Added + +* `--log-level=none` command-line option. +* `Tc`, `setrgbf` and `setrgbb` capabilities in `foot` and `foot-direct` + terminfo entries. This should make 24-bit RGB colors work in tmux and + neovim, without the need for config hacks or detection heuristics + ([#615](https://codeberg.org/dnkl/foot/issues/615)). + + +### Changed + +* Grapheme cluster width is now limited to two cells by default. This + may cause cursor synchronization issues with many applications. You + can set `[tweak].grapheme-width-method=wcswidth` to revert to the + behavior in foot-1.8.0. + + +### Fixed + +* Grapheme cluster state being reset between codepoints. +* Regression: custom URL key bindings not working + ([#614](https://codeberg.org/dnkl/foot/issues/614)). + + +### Contributors + +* [craigbarnes](https://codeberg.org/craigbarnes) + + +## 1.8.0 + +### Grapheme shaping + +This release adds _experimental, opt-in_ support for grapheme cluster +segmentation and grapheme shaping. + +(note: several of the examples below may not render correctly in your +browser, viewer or editor). + +Grapheme cluster segmentation is the art of splitting up text into +grapheme clusters, where a cluster may consist of more than one +Unicode codepoint. For example, 🙂 is a single codepoint, while 👩🏽‍🚀 +consists of 4 codepoints (_Woman_ + _Medium skin tone_ + _Zero width +joiner_ + _Rocket_). The goal is to _cluster_ codepoints belonging to +the same grapheme in the same cell in the terminal. + +Previous versions of foot implemented a simple grapheme cluster +segmentation technique that **only** handled zero-width +codepoints. This allowed us to cluster combining characters, like q́ +(_q_ + _COMBINING ACUTE ACCENT_). + +Once we have a grapheme cluster, we need to _shape_ it. + +Combining characters are simple: they are typically rendered as +multiple glyphs layered on top of each other. This is why previous +versions of foot got away with it without any actual text shaping +support. + +Beyond that, support from the font library is needed. Foot now depends +on fcft-2.4, which added support for grapheme and text shaping. When +rendering a cell, we ask the font library: give us the glyph(s) for +this sequence of codepoints. + +Fancy emoji sequences aside, using libutf8proc for grapheme cluster +segmentation means **improved correctness**. + +For full support, the following is required: + +* fcft compiled with HarfBuzz support +* foot compiled with libutf8proc support +* `tweak.grapheme-shaping=yes` in `foot.ini` + +If `tweak.grapheme-shaping` has **not** been enabled, foot will +neither use libutf8proc to do grapheme cluster segmentation, nor will +it use fcft's grapheme shaping capabilities to shape combining +characters. + +This feature is _experimental_ mostly due to the "wcwidth" problem; +how many cells should foot allocate for a grapheme cluster? While the +answer may seem simple, the problem is that, whatever the answer is, +the client application **must** come up with the **same** +answer. Otherwise we get cursor synchronization issues. + +In this release, foot simply adds together the `wcwidth()` of all +codepoints in the grapheme cluster. This is equivalent to running +`wcswidth()` on the entire cluster. **This is likely to change in the +future**. + +Finally, note that grapheme shaping is not the same thing as text (or +text run) shaping. In this version, foot only shapes individual +graphemes, not entire text runs. That means e.g. ligatures are **not** +supported. + + +### Added + +* Support for DECSET/DECRST 2026, as an alternative to the existing + "synchronized updates" DCS sequences + ([#459](https://codeberg.org/dnkl/foot/issues/459)). +* `cursor.beam-thickness` option to `foot.ini` + ([#464](https://codeberg.org/dnkl/foot/issues/464)). +* `cursor.underline-thickness` option to `foot.ini` + ([#524](https://codeberg.org/dnkl/foot/issues/524)). +* Unicode 13 characters U+1FB70 - U+1FB8B to list of box drawing + characters rendered by foot itself (rather than using font glyphs) + ([#471](https://codeberg.org/dnkl/foot/issues/471)). +* Dedicated `[bell]` section to config, supporting multiple actions + and a new `command` action to run an arbitrary command. + (https://codeberg.org/dnkl/foot/pulls/483) +* Dedicated `[url]` section to config. +* `[url].protocols` option to `foot.ini` + ([#531](https://codeberg.org/dnkl/foot/issues/531)). +* Support for setting the full 256 color palette in foot.ini + ([#489](https://codeberg.org/dnkl/foot/issues/489)) +* XDG activation support, will be used by `[bell].urgent` when + available (falling back to coloring the window margins red when + unavailable) ([#487](https://codeberg.org/dnkl/foot/issues/487)). +* `ctrl`+`c` as a default key binding; to cancel search/url mode. +* `${window-title}` to `notify`. +* Support for including files in `foot.ini` + ([#555](https://codeberg.org/dnkl/foot/issues/555)). +* `ENVIRONMENT` section in **foot**(1) and **footclient**(1) man pages + ([#556](https://codeberg.org/dnkl/foot/issues/556)). +* `tweak.pua-double-width` option to `foot.ini`, letting you force + _Private Usage Area_ codepoints to be treated as double-width + characters. +* OSC 9 desktop notifications (iTerm2 compatible). +* Support for LS2 and LS3 (locking shift) escape sequences + ([#581](https://codeberg.org/dnkl/foot/issues/581)). +* Support for overriding configuration options on the command line + ([#554](https://codeberg.org/dnkl/foot/issues/554), + [#600](https://codeberg.org/dnkl/foot/issues/600)). +* `underline-offset` option to `foot.ini` + ([#490](https://codeberg.org/dnkl/foot/issues/490)). +* `csd.button-color` option to `foot.ini`. +* `-Dterminfo-install-location=disabled|` meson command + line option ([#569](https://codeberg.org/dnkl/foot/issues/569)). + + +### Changed + +* [fcft](https://codeberg.org/dnkl/fcft): required version bumped from + 2.3.x to 2.4.x. +* `generate-alt-random-writes.py --sixel`: width and height of emitted + sixels has been adjusted. +* _Concealed_ text (`\E[8m`) is now revealed when highlighted. +* The background color of highlighted text is now adjusted, when the + foreground and background colors are the same, making the + highlighted text legible + ([#455](https://codeberg.org/dnkl/foot/issues/455)). +* `cursor.style=bar` to `cursor.style=beam`. `bar` remains a + recognized value, but will eventually be deprecated, and removed. +* Point values in `line-height`, `letter-spacing`, + `horizontal-letter-offset` and `vertical-letter-offset` are now + rounded, not truncated, when translated to pixel values. +* Foot's exit code is now -26/230 when foot itself failed to launch + (due to invalid command line options, client application/shell not + found etc). Footclient's exit code is -36/220 when it itself fails + to launch (e.g. bad command line option) and -26/230 when the foot + server failed to instantiate a new window + ([#466](https://codeberg.org/dnkl/foot/issues/466)). +* Background alpha no longer applied to palette or RGB colors that + matches the background color. +* Improved performance on compositors that does not release shm + buffers immediately, e.g. KWin + ([#478](https://codeberg.org/dnkl/foot/issues/478)). +* `ctrl + w` (_extend-to-word-boundary_) can now be used across lines + ([#421](https://codeberg.org/dnkl/foot/issues/421)). +* Ignore auto-detected URLs that overlap with OSC-8 URLs. +* Default value for the `notify` option to use `-a ${app-id} -i + ${app-id} ...` instead of `-a foot -i foot ...`. +* `scrollback-*`+`pipe-scrollback` key bindings are now passed through + to the client application when the alt screen is active + ([#573](https://codeberg.org/dnkl/foot/issues/573)). +* Reverse video (`\E[?5h`) now only swaps the default foreground and + background colors. Cells with explicit foreground and/or background + colors remain unchanged. +* Tabs (`\t`) are now preserved when the window is resized, and when + copying text ([#508](https://codeberg.org/dnkl/foot/issues/508)). +* Writing a sixel on top of another sixel no longer erases the first + sixel, but the two are instead blended + ([#562](https://codeberg.org/dnkl/foot/issues/562)). +* Running foot without a configuration file is no longer an error; it + has been demoted to a warning, and is no longer presented as a + notification in the terminal window, but only logged on stderr. + + +### Deprecated + +* `bell` option in `foot.ini`; set actions in the `[bell]` section + instead. +* `url-launch` option in `foot.ini`; use `launch` in the `[url]` + section instead. +* `jump-label-letters` option in `foot.ini`; use `label-letters` in + the `[url]` section instead. +* `osc8-underline` option in `foot.ini`; use `osc8-underline` in the + `[url]` section instead. + + +### Removed + +* Buffer damage quirk for Plasma/KWin. + + +### Fixed + +* `generate-alt-random-writes.py --sixel` sometimes crashing, + resulting in PGO build failures. +* Wrong colors in the 256-color cube + ([#479](https://codeberg.org/dnkl/foot/issues/479)). +* Memory leak triggered by "opening" an OSC-8 URI and then resetting + the terminal without closing the URI + ([#495](https://codeberg.org/dnkl/foot/issues/495)). +* Assertion when emitting a sixel occupying the entire scrollback + history ([#494](https://codeberg.org/dnkl/foot/issues/494)). +* Font underlines being positioned below the cell (and thus being + invisible) for certain combinations of fonts and font sizes + ([#503](https://codeberg.org/dnkl/foot/issues/503)). +* Sixels with transparent bottom border being resized below the size + specified in _"Set Raster Attributes"_. +* Fonts sometimes not being reloaded with the correct scaling factor + when `dpi-aware=no`, or `dpi-aware=auto` with monitor(s) with a + scaling factor > 1 ([#509](https://codeberg.org/dnkl/foot/issues/509)). +* Crash caused by certain CSI sequences with very large parameter + values ([#522](https://codeberg.org/dnkl/foot/issues/522)). +* Rare occurrences where the window did not close when the shell + exited. Only seen on FreeBSD + ([#534](https://codeberg.org/dnkl/foot/issues/534)) +* Foot process(es) sometimes remaining, using 100% CPU, when closing + multiple foot windows at the same time + ([#542](https://codeberg.org/dnkl/foot/issues/542)). +* Regression where `+shift+tab` always produced `\E[Z` instead of + the correct `\E[27;;9~` sequence + ([#547](https://codeberg.org/dnkl/foot/issues/547)). +* Crash when a line wrapping OSC-8 URI crossed the scrollback wrap + around ([#552](https://codeberg.org/dnkl/foot/issues/552)). +* Selection incorrectly wrapping rows ending with an explicit newline + ([#565](https://codeberg.org/dnkl/foot/issues/565)). +* Off-by-one error in markup of auto-detected URLs when the URL ends + in the right-most column. +* Multi-column characters being cut in half when resizing the + alternate screen. +* Restore `SIGHUP` in spawned processes. +* Text reflow performance ([#504](https://codeberg.org/dnkl/foot/issues/504)). +* IL+DL (`CSI Ps L` + `CSI Ps M`) now moves the cursor to column 0. +* SS2 and SS3 (single shift) escape sequences behaving like locking + shifts ([#580](https://codeberg.org/dnkl/foot/issues/580)). +* `TEXT`+`STRING`+`UTF8_STRING` mime types not being recognized in + clipboard offers ([#583](https://codeberg.org/dnkl/foot/issues/583)). +* Memory leak caused by custom box drawing glyphs not being completely + freed when destroying a foot window instance + ([#586](https://codeberg.org/dnkl/foot/issues/586)). +* Crash in scrollback search when current XKB layout is missing + _compose_ definitions. +* Window title not being updated while window is hidden + ([#591](https://codeberg.org/dnkl/foot/issues/591)). +* Crash on badly formatted URIs in e.g. OSC-8 URLs. +* Window being incorrectly resized on CSD/SSD run-time changes. + + +### Contributors +* [r\_c\_f](https://codeberg.org/r_c_f) +* [craigbarnes](https://codeberg.org/craigbarnes) + + +## 1.7.2 + +### Added + +* URxvt OSC-11 extension to set background alpha + ([#436](https://codeberg.org/dnkl/foot/issues/436)). +* OSC 17/117/19/119 - change/reset selection background/foreground + color. +* `box-drawings-uses-font-glyphs=yes|no` option to `foot.ini` + ([#430](https://codeberg.org/dnkl/foot/issues/430)). + + +### Changed + +* Underline cursor is now rendered below text underline + ([#415](https://codeberg.org/dnkl/foot/issues/415)). +* Foot now tries much harder to keep URL jump labels inside the window + geometry ([#443](https://codeberg.org/dnkl/foot/issues/443)). +* `bold-text-in-bright` may now be set to `palette-based`, in which + case it will use the corresponding bright palette color when the + color to brighten matches one of the base 8 colors, instead of + increasing the luminance + ([#449](https://codeberg.org/dnkl/foot/issues/449)). + + +### Fixed + +* Reverted _"Consumed modifiers are no longer sent to the client + application"_ ([#425](https://codeberg.org/dnkl/foot/issues/425)). +* Crash caused by a double free originating in `XTSMGRAPHICS` - set + number of color registers + ([#427](https://codeberg.org/dnkl/foot/issues/427)). +* Wrong action referenced in error message for key binding collisions + ([#432](https://codeberg.org/dnkl/foot/issues/432)). +* OSC 4/104 out-of-bounds accesses to the color table. This was the + reason pywal turned foot windows transparent + ([#434](https://codeberg.org/dnkl/foot/issues/434)). +* PTY not being drained when the client application terminates. +* `auto_left_margin` not being limited to `cub1` + ([#441](https://codeberg.org/dnkl/foot/issues/441)). +* Crash in scrollback search mode when searching beyond the last output. + + +### Contributors + +* [cglogic](https://codeberg.org/cglogic) + + +## 1.7.1 + +### Changed + +* Update PGO build instructions in `INSTALL.md` + ([#418](https://codeberg.org/dnkl/foot/issues/418)). +* In scrollback search mode, empty cells can now be matched by spaces. + + +### Fixed + +* Logic that repairs invalid key bindings ended up breaking valid key + bindings instead ([#407](https://codeberg.org/dnkl/foot/issues/407)). +* Custom `line-height` settings now scale when increasing or + decreasing the font size at run-time. +* Newlines sometimes incorrectly inserted into copied text + ([#410](https://codeberg.org/dnkl/foot/issues/410)). +* Crash when compositor send `text-input-v3::enter` events without + first having sent a `keyboard::enter` event + ([#411](https://codeberg.org/dnkl/foot/issues/411)). +* Deadlock when rendering sixel images. +* URL labels, scrollback search box or scrollback position indicator + sometimes not showing up, caused by invalidly sized surface buffers + when output scaling was enabled + ([#409](https://codeberg.org/dnkl/foot/issues/409)). +* Empty sixels resulted in non-empty images. + + +## 1.7.0 + +### Added + +* The `pad` option now accepts an optional third argument, `center` + (e.g. `pad=5x5 center`), causing the grid to be centered in the + window, with equal amount of padding of the left/right and + top/bottom side ([#273](https://codeberg.org/dnkl/foot/issues/273)). +* `line-height`, `letter-spacing`, `horizontal-letter-offset` and + `vertical-letter-offset` to `foot.ini`. These options let you tweak + cell size and glyph positioning + ([#244](https://codeberg.org/dnkl/foot/issues/244)). +* Key/mouse binding `select-extend-character-wise`, which forces the + selection mode to 'character-wise' when extending a selection. +* `DECSET` `47`, `1047` and `1048`. +* URL detection and OSC-8 support. URLs are highlighted and activated + using the keyboard (**no** mouse support). See **foot**(1)::URLs, or + [README.md](README.md#urls) for details + ([#14](https://codeberg.org/dnkl/foot/issues/14)). +* `-d,--log-level={info|warning|error}` to both `foot` and + `footclient` ([#337](https://codeberg.org/dnkl/foot/issues/337)). +* `-D,--working-directory=DIR` to both `foot` and `footclient` + ([#347](https://codeberg.org/dnkl/foot/issues/347)) +* `DECSET 80` - sixel scrolling + ([#361](https://codeberg.org/dnkl/foot/issues/361)). +* `DECSET 1070` - sixel private color palette + ([#362](https://codeberg.org/dnkl/foot/issues/362)). +* `DECSET 8452` - position cursor to the right of sixels + ([#363](https://codeberg.org/dnkl/foot/issues/363)). +* Man page **foot-ctlseqs**(7), documenting all supported escape + sequences ([#235](https://codeberg.org/dnkl/foot/issues/235)). +* Support for transparent sixels (DCS parameter `P2=1`) + ([#391](https://codeberg.org/dnkl/foot/issues/391)). +* `-N,--no-wait` to `footclient` + ([#395](https://codeberg.org/dnkl/foot/issues/395)). +* Completions for Bash shell + ([#10](https://codeberg.org/dnkl/foot/issues/10)). +* Implement `XTVERSION` (`CSI > 0q`). Foot will reply with + `DCS>|foot(..)ST` + ([#359](https://codeberg.org/dnkl/foot/issues/359)). + + +### Changed + +* The fcft and tllist library subprojects are now handled via Meson + [wrap files](https://mesonbuild.com/Wrap-dependency-system-manual.html) + instead of needing to be manually cloned. +* Box drawing characters are now rendered by foot, instead of using + font glyphs ([#198](https://codeberg.org/dnkl/foot/issues/198)) +* Double- or triple clicking then dragging now extends the selection + word- or line-wise ([#267](https://codeberg.org/dnkl/foot/issues/267)). +* The line thickness of box drawing characters now depend on the font + size ([#281](https://codeberg.org/dnkl/foot/issues/281)). +* Extending a word/line-wise selection now uses the original selection + mode instead of switching to character-wise. +* While doing an interactive resize of a foot window, foot now + requires 100ms of idle time (where the window size does not change) + before sending the new dimensions to the client application. The + timing can be tweaked, or completely disabled, by setting + `resize-delay-ms` ([#301](https://codeberg.org/dnkl/foot/issues/301)). +* `CSI 13 ; 2 t` now reports (0,0). +* Key binding matching logic; key combinations like `Control+Shift+C` + **must** now be written as either `Control+C` or `Control+Shift+c`, + the latter being the preferred + variant. ([#376](https://codeberg.org/dnkl/foot/issues/376)) +* Consumed modifiers are no longer sent to the client application + ([#376](https://codeberg.org/dnkl/foot/issues/376)). +* The minimum version requirement for the libxkbcommon dependency is + now 1.0.0. +* Empty pixel rows at the bottom of a sixel is now trimmed. +* Sixels with DCS parameter `P2=0|2` now use the _current_ ANSI + background color for empty pixels instead of the default background + color ([#391](https://codeberg.org/dnkl/foot/issues/391)). +* Sixel decoding optimized; up to 100% faster in some cases. +* Reported sixel "max geometry" from current window size, to the + configured maximum size (defaulting to 10000x10000). + + +### Removed + +* The `-g,--geometry` command-line option (which had been deprecated + and superseded by `-w,--window-size-pixels` since 1.5.0). + + +### Fixed + +* Some mouse bindings (_primary paste_, for example) did not require + `shift` to be pressed while used in a mouse grabbing + application. This meant the mouse event was never seen by the + application. +* Terminals spawned with `ctrl`+`shift`+`n` not terminating when + exiting shell ([#366](https://codeberg.org/dnkl/foot/issues/366)). +* Default value of `-t,--term` in `--help` output when foot was built + without terminfo support. +* Drain PTY when the client application terminates. + + +### Contributors + +* [craigbarnes](https://codeberg.org/craigbarnes) +* toast +* [l3mon4d3](https://codeberg.org/l3mon4d3) +* [Simon Schricker](mailto:s.schricker@sillage.at) + + +## 1.6.4 + +### Added + +* `selection-target=none|primary|clipboard|both` to `foot.ini`. It can + be used to configure which clipboard(s) selected text should be + copied to. The default is `primary`, which corresponds to the + behavior in older foot releases + ([#288](https://codeberg.org/dnkl/foot/issues/288)). + + +### Changed + +* The IME state no longer stays stuck in the terminal if the IME goes + away during preedit. +* `-Dterminfo` changed from a `boolean` to a `feature` option. +* Use standard signals instead of a signalfd to handle + `SIGCHLD`. Fixes an issue on FreeBSD where foot did not detect when + the client application had terminated. + + +### Fixed + +* `BS`, `HT` and `DEL` from being stripped in bracketed paste mode. + + +### Contributors + +* [tdeo](https://codeberg.org/tdeo) +* jbeich + + +## 1.6.3 + +### Added + +* Completions for fish shell + ([#11](https://codeberg.org/dnkl/foot/issues/11)) +* FreeBSD support ([#238](https://codeberg.org/dnkl/foot/issues/238)). +* IME popup location support: foot now sends the location of the cursor + so any popup can be displayed near the text that is being typed. + + +### Changed + +* Trailing comments in `foot.ini` must now be preceded by a space or tab + ([#270](https://codeberg.org/dnkl/foot/issues/270)) +* The scrollback search box no longer accepts non-printable characters. +* Non-formatting C0 control characters, `BS`, `HT` and `DEL` are now + stripped from pasted text. + + +### Fixed + +* Exit when the client application terminates, not when the TTY file + descriptor is closed. +* Crash on compositors not implementing the _text input_ interface + ([#259](https://codeberg.org/dnkl/foot/issues/259)). +* Erased, overflowing glyphs (when + `tweak.allow-overflowing-double-width-glyphs=yes` - the default) not + properly erasing the cell overflowed **into**. +* `word-delimiters` option ignores `#` and subsequent characters + ([#270](https://codeberg.org/dnkl/foot/issues/270)) +* Combining characters not being rendered when composed with colored + bitmap glyphs (i.e. colored emojis). +* Pasting URIs from the clipboard when the source has not + newline-terminated the last URI + ([#291](https://codeberg.org/dnkl/foot/issues/291)). +* Sixel "current geometry" query response not being bounded by the + current window dimensions (fixes `lsix` output) +* Crash on keyboard input when repeat rate was zero (i.e. no repeat). +* Wrong button encoding of mouse buttons 6 and 7 in mouse events. +* Scrollback search not matching composed characters. +* High CPU usage when holding down e.g. arrow keys while in scrollback + search mode. +* Rendering of composed characters in the scrollback search box. +* IME pre-edit cursor when positioned at the end of the pre-edit + string. +* Scrollback search not matching multi-column characters. + + +### Contributors + +* [pc](https://codeberg.org/pc) +* [FollieHiyuki](https://codeberg.org/FollieHiyuki) +* jbeich +* [tdeo](https://codeberg.org/tdeo) + + +## 1.6.2 + +### Fixed + +* Version number in `meson.build`. + + +## 1.6.1 +### Added + +* `--seed` to `generate-alt-random.py`, enabling deterministic PGO + builds. + + +### Changed + + +* Use `-std=c11` instead of `-std=c18`. +* Added `-Wno-profile-instr-unprofiled` to Clang cflags in PGO builds + ([INSTALL.md](https://codeberg.org/dnkl/foot/src/branch/releases/1.6/INSTALL.md#user-content-performance-optimized-pgo)) + + +### Fixed + +* Missing dependencies in meson, causing heavily parallelized builds + to fail. +* Background color when alpha < 1.0 being wrong + ([#249](https://codeberg.org/dnkl/foot/issues/249)). +* `generate-alt-random.py` failing in containers. + + +### Contributors + +* [craigbarnes](https://codeberg.org/craigbarnes) +* [sterni](https://codeberg.org/sterni) + + +## 1.6.0 + +### For packagers + +Starting with this release, foot can be PGO:d (compiled using profile +guided optimizations) **without** a running Wayland session. This +means foot can be PGO:d in e.g. sandboxed build scripts. See +[INSTALL.md](INSTALL.md#user-content-performance-optimized-pgo). + + +### Added + +* IME support. This is compile-time optional, see + [INSTALL.md](INSTALL.md#user-content-options) + ([#134](https://codeberg.org/dnkl/foot/issues/134)). +* `DECSET` escape to enable/disable IME: `CSI ? 737769 h` enables IME + and `CSI ? 737769 l` disables it. This can be used to + e.g. enable/disable IME when entering/leaving insert mode in vim. +* `dpi-aware` option to `foot.ini`. The default, `auto`, sizes fonts + using the monitor's DPI when output scaling has been + **disabled**. If output scaling has been **enabled**, fonts are + sized using the scaling factor. DPI-only font sizing can be forced + by setting `dpi-aware=yes`. Setting `dpi-aware=no` forces font + sizing to be based on the scaling factor. + ([#206](https://codeberg.org/dnkl/foot/issues/206)). +* Implement reverse auto-wrap (_auto\_left\_margin_, _bw_, in + terminfo). This mode can be enabled/disabled with `CSI ? 45 h` and + `CSI ? 45 l`. It is **enabled** by default + ([#150](https://codeberg.org/dnkl/foot/issues/150)). +* `bell` option to `foot.ini`. Can be set to `set-urgency` to make + foot render the margins in red when receiving `BEL` while **not** + having keyboard focus. Applications can dynamically enable/disable + this with the `CSI ? 1042 h` and `CSI ? 1042 l` escape + sequences. Note that Wayland does **not** implement an _urgency_ + hint like X11, but that there is a + [proposal](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/9) + to add support for this. The value `set-urgency` was chosen for + forward-compatibility, in the hopes that this proposal eventualizes + ([#157](https://codeberg.org/dnkl/foot/issues/157)). +* `bell` option can also be set to `notify`, in which case a desktop + notification is emitted when foot receives `BEL` in an unfocused + window. +* `word-delimiters` option to `foot.ini` + ([#156](https://codeberg.org/dnkl/foot/issues/156)). +* `csd.preferred` can now be set to `none` to disable window + decorations. Note that some compositors will render SSDs despite + this option being used ([#163](https://codeberg.org/dnkl/foot/issues/163)). +* Terminal content is now auto-scrolled when moving the mouse above or + below the window while selecting + ([#149](https://codeberg.org/dnkl/foot/issues/149)). +* `font-bold`, `font-italic` `font-bold-italic` options to + `foot.ini`. These options allow custom bold/italic fonts. They are + unset by default, meaning the bold/italic version of the regular + font is used ([#169](https://codeberg.org/dnkl/foot/issues/169)). +* Drag & drop support; text, files and URLs can now be dropped in a + foot terminal window ([#175](https://codeberg.org/dnkl/foot/issues/175)). +* `clipboard-paste` and `primary-paste` scrollback search bindings. By + default, they are bound to `ctrl+v ctrl+y` and `shift+insert` + respectively, and lets you paste from the clipboard or primary + selection into the search buffer. +* Support for `pipe-*` actions in mouse bindings. It was previously + not possible to add a command to these actions when used in mouse + bindings, making them useless + ([#183](https://codeberg.org/dnkl/foot/issues/183)). +* `bold-text-in-bright` option to `foot.ini`. When enabled, bold text + is rendered in a brighter color + ([#199](https://codeberg.org/dnkl/foot/issues/199)). +* `-w,--window-size-pixels` and `-W,--window-size-chars` command line + options to `footclient` ([#189](https://codeberg.org/dnkl/foot/issues/189)). +* Short command line options for `--title`, `--maximized`, + `--fullscreen`, `--login-shell`, `--hold` and `--check-config`. +* `DECSET` escape to modify the `escape` key to send `\E[27;1;27~` + instead of `\E`: `CSI ? 27127 h` enables the new behavior, `CSI ? + 27127 l` disables it (the default). +* OSC 777;notify: desktop notifications. Use in combination with the + new `notify` option in `foot.ini` + ([#224](https://codeberg.org/dnkl/foot/issues/224)). +* Status line terminfo capabilities `hs`, `tsl`, `fsl` and `dsl`. This + enables e.g. vim to set the window title + ([#242](https://codeberg.org/dnkl/foot/issues/242)). + + +### Changed + +* Blinking text now uses the foreground color, but dimmed down in its + off state, instead of the background color. +* Sixel default maximum size is now 10000x10000 instead of the current + window size. +* Graphical glitches/flashes when resizing the window while running a + fullscreen application, i.e. the 'alt' screen + ([#221](https://codeberg.org/dnkl/foot/issues/221)). +* Cursor will now blink if **either** `CSI ? 12 h` or `CSI Ps SP q` + has been used to enable blinking. **cursor.blink** in `foot.ini` + controls the default state of `CSI Ps SP q` + ([#218](https://codeberg.org/dnkl/foot/issues/218)). +* The sub-parameter versions of the SGR RGB color escapes (e.g + `\E[38:2...m`) can now be used _without_ the color space ID + parameter. +* SGR 21 no longer disables **bold**. According to ECMA-48, SGR 21 is + _"double underline_". Foot does not (yet) implement that, but that's + no reason to implement a non-standard behavior. +* `DECRQM` now returns actual state of the requested mode, instead of + always returning `2`. + + +### Removed + +* Support for loading configuration from `$XDG_CONFIG_HOME/footrc`. +* `scrollback` option from `foot.ini`. +* `geometry` from `foot.ini`. +* Key binding action `scrollback-up` and `scrollback-down`. + + +### Fixed + +* Error when re-assigning a default key binding + ([#233](https://codeberg.org/dnkl/foot/issues/233)). +* `\E[s`+`\E[u` (save/restore cursor) now saves and restores + attributes and charset configuration, just like `\E7`+`\E8`. +* Report mouse motion events to the client application also while + dragging the cursor outside the grid. +* Parsing of the sub-parameter versions of indexed SGR color escapes + (e.g. `\E[38:5...m`) +* Frames occasionally being rendered while application synchronized + updates is in effect. +* Handling of failures to parse the font specification string. +* Extra private/intermediate characters in escape sequences not being + ignored. + + +### Contributors + +* [kennylevinsen](https://codeberg.org/kennylevinsen) +* [craigbarnes](https://codeberg.org/craigbarnes) + + +## 1.5.4 + +### Changed + + +* Num Lock by default overrides the keypad mode. See + **foot.ini**(5)::KEYPAD, or + [README.md](README.md#user-content-keypad) for details + ([#194](https://codeberg.org/dnkl/foot/issues/194)). +* Single-width characters with double-width glyphs are now allowed to + overflow into neighboring cells by default. Set + **tweak.allow-overflowing-double-width-glyphs** to 'no' to disable + this. + +### Fixed + +* Resize very slow when window is hidden + ([#190](https://codeberg.org/dnkl/foot/issues/190)). +* Key mappings for key combinations with `shift`+`tab` + ([#210](https://codeberg.org/dnkl/foot/issues/210)). +* Key mappings for key combinations with `alt`+`return`. +* `footclient` `-m` (`--maximized`) flag being ignored. +* Crash with explicitly sized sixels with a height less than 6 pixels. +* Key mappings for `esc` with modifiers. + + +### Contributors + +* [craigbarnes](https://codeberg.org/craigbarnes) + + +## 1.5.3 + +### Fixed + +* Crash when libxkbcommon cannot find a suitable libX11 _compose_ + file. Note that foot will run, but without support for dead keys. + ([#170](https://codeberg.org/dnkl/foot/issues/170)). +* Restored window size when window is un-tiled. +* XCursor shape in CSD corners when window is tiled. +* Error handling when processing keyboard input (maybe + [#171](https://codeberg.org/dnkl/foot/issues/171)). +* Compilation error _"overflow in conversion from long 'unsigned int' + to 'int' changes value... "_ seen on platforms where the `request` + argument in `ioctl(3)` is an `int` (for example: linux/ppc64). +* Crash when using the mouse in alternate scroll mode in an unfocused + window ([#179](https://codeberg.org/dnkl/foot/issues/179)). +* Character dropped from selection when "right-click-hold"-extending a + selection ([#180](https://codeberg.org/dnkl/foot/issues/180)). + + +## 1.5.2 + +### Fixed + +* Regression: middle clicking double pastes in e.g. vim + ([#168](https://codeberg.org/dnkl/foot/issues/168)) + + +## 1.5.1 + +### Changed + +* Default value of the **scrollback.multiplier** option in `foot.ini` + from `1.0` to `3.0`. +* `shift`+`insert` now pastes from the primary selection by + default. This is in addition to middle-clicking with the mouse. + + +### Fixed + +* Mouse bindings now match even if the actual click count is larger + than specified in the binding. This allows you to, for example, + quickly press the middle-button to paste multiple times + ([#146](https://codeberg.org/dnkl/foot/issues/146)). +* Color flashes when changing the color palette with OSC 4,10,11 + ([#141](https://codeberg.org/dnkl/foot/issues/141)). +* Scrollback position is now retained when resizing the window + ([#142](https://codeberg.org/dnkl/foot/issues/142)). +* Trackpad scrolling speed to better match the mouse scrolling speed, + and to be consistent with other (Wayland) terminal emulators. Note + that it is (much) slower compared to previous foot versions. Use the + **scrollback.multiplier** option in `foot.ini` if you find the new + speed too slow ([#144](https://codeberg.org/dnkl/foot/issues/144)). +* Crash when `foot.ini` contains an invalid section name + ([#159](https://codeberg.org/dnkl/foot/issues/159)). +* Background opacity when in _reverse video_ mode. +* Crash when writing a sixel image that extends outside the terminal's + right margin ([#151](https://codeberg.org/dnkl/foot/issues/151)). +* Sixel image at non-zero column positions getting sheared at + seemingly random occasions + ([#151](https://codeberg.org/dnkl/foot/issues/151)). +* Crash after either resizing a window or changing the font size if + there were sixels present in the scrollback while doing so. +* _Send Device Attributes_ to only send a response if `Ps == 0`. +* Paste from primary when clipboard is empty. + + +### Contributors + +* [craigbarnes](https://codeberg.org/craigbarnes) +* [zar](https://codeberg.org/zar) + + +## 1.5.0 + +### Deprecated + +* `$XDG_CONFIG_HOME/footrc`/`~/.config/footrc`. Use + `$XDG_CONFIG_HOME/foot/foot.ini`/`~/.config/foot/foot.ini` instead. +* **scrollback** option in `foot.ini`. Use **scrollback.lines** + instead. +* **scrollback-up** key binding. Use **scrollback-up-page** instead. +* **scrollback-down** key binding. Use **scrollback-down-page** + instead. + + +### Added + +* Scrollback position indicator. This feature is optional and + controlled by the **scrollback.indicator-position** and + **scrollback.indicator-format** options in `foot.ini` + ([#42](https://codeberg.org/dnkl/foot/issues/42)). +* Key bindings in _scrollback search_ mode are now configurable. +* `--check-config` command line option. +* **pipe-selected** key binding. Works like **pipe-visible** and + **pipe-scrollback**, but only pipes the currently selected text, if + any ([#51](https://codeberg.org/dnkl/foot/issues/51)). +* **mouse.hide-when-typing** option to `foot.ini`. +* **scrollback.multiplier** option to `foot.ini` + ([#54](https://codeberg.org/dnkl/foot/issues/54)). +* **colors.selection-foreground** and **colors.selection-background** + options to `foot.ini`. +* **tweak.render-timer** option to `foot.ini`. +* Modifier support in mouse bindings + ([#77](https://codeberg.org/dnkl/foot/issues/77)). +* Click count support in mouse bindings, i.e double- and triple-click + ([#78](https://codeberg.org/dnkl/foot/issues/78)). +* All mouse actions (begin selection, select word, select row etc) are + now configurable, via the new **select-begin**, + **select-begin-block**, **select-extend**, **select-word**, + **select-word-whitespace** and **select-row** options in the + **mouse-bindings** section in `foot.ini` + ([#79](https://codeberg.org/dnkl/foot/issues/79)). +* Implement XTSAVE/XTRESTORE escape sequences, `CSI ? Ps s` and `CSI ? + Ps r` ([#91](https://codeberg.org/dnkl/foot/issues/91)). +* `$COLORTERM` is now set to `truecolor` at startup, to indicate + support for 24-bit RGB colors. +* Experimental support for rendering double-width glyphs with a + character width of 1. Must be explicitly enabled with + `tweak.allow-overflowing-double-width-glyphs` + ([#116](https://codeberg.org/dnkl/foot/issues/116)). +* **initial-window-size-pixels** options to `foot.ini` and + `-w,--window-size-pixels` command line option to `foot`. This option + replaces the now deprecated **geometry** and `-g,--geometry` + options. +* **initial-window-size-chars** option to `foot.ini` and + `-W,--window-size-chars` command line option to `foot`. This option + configures the initial window size in **characters**, and is an + alternative to **initial-window-size-pixels**. +* **scrollback-up-half-page** and **scrollback-down-half-page** key + bindings. They scroll up/down half of a page in the scrollback + ([#128](https://codeberg.org/dnkl/foot/issues/128)). +* **scrollback-up-line** and **scrollback-down-line** key + bindings. They scroll up/down a single line in the scrollback. +* **mouse.alternate-scroll-mode** option to `foot.ini`. This option + controls the initial state of the _Alternate Scroll Mode_, and + defaults to `yes`. When enabled, mouse scroll events are translated + to up/down key events in the alternate screen, letting you scroll in + e.g. `less` and other applications without enabling native mouse + support in them ([#135](https://codeberg.org/dnkl/foot/issues/135)). + + +### Changed + +* Renamed man page for `foot.ini` from **foot**(5) to **foot.ini**(5). +* Configuration errors are no longer fatal; foot will start and print + an error inside the terminal (and of course still log errors on + stderr). +* Default `--server` socket path to use `$WAYLAND_DISPLAY` instead of + `$XDG_SESSION_ID` ([#55](https://codeberg.org/dnkl/foot/issues/55)). +* Trailing empty cells are no longer highlighted in mouse selections. +* Foot now searches for its configuration in + `$XDG_DATA_DIRS/foot/foot.ini`, if no configuration is found in + `$XDG_CONFIG_HOME/foot/foot.ini` or in `$XDG_CONFIG_HOME/footrc`. +* Minimum window size changed from four rows and 20 columns, to 1 row + and 2 columns. +* **scrollback-up/down** renamed to **scrollback-up/down-page**. +* fcft >= 2.3.0 is now required. + + +### Fixed + +* Command lines for **pipe-visible** and **pipe-scrollback** are now + tokenized (i.e. syntax checked) when the configuration is loaded, + instead of every time the key binding is executed. +* Incorrect multi-column character spacer insertion when reflowing + text. +* Compilation errors in 32-bit builds. +* Mouse cursor style in top and left margins. +* Selection is now **updated** when the cursor moves outside the grid + ([#70](https://codeberg.org/dnkl/foot/issues/70)). +* Viewport sometimes not moving when doing a scrollback search. +* Crash when canceling a scrollback search and the window had been + resized while searching. +* Selection start point not moving when the selection changes + direction. +* OSC 10/11/104/110/111 (modify colors) did not update existing screen + content ([#94](https://codeberg.org/dnkl/foot/issues/94)). +* Extra newlines when copying empty cells + ([#97](https://codeberg.org/dnkl/foot/issues/97)). +* Mouse events from being sent to client application when a mouse + binding has consumed it. +* Input events from getting mixed with paste data + ([#101](https://codeberg.org/dnkl/foot/issues/101)). +* Missing DPI values for "some" monitors on Gnome + ([#118](https://codeberg.org/dnkl/foot/issues/118)). +* Handling of multi-column composed characters while reflowing. +* Escape sequences sent for key combinations with `Return`, that did + **not** include `Alt`. +* Clipboard (or primary selection) is now cleared when receiving an + OSC-52 command with an invalid base64 encoded payload. +* Cursor position being set outside the grid when reflowing text. +* CSD buttons to be hidden when window size becomes so small that they + no longer fit. + + +### Contributors + +* [craigbarnes](https://codeberg.org/craigbarnes) +* [birger](https://codeberg.org/birger) +* [Ordoviz](https://codeberg.org/Ordoviz) +* [cherti](https://codeberg.org/cherti) + + +## 1.4.4 +### Changed + +* Mouse cursor is now always a `left_ptr` when inside the margins, to + indicate it is not possible to start a selection. + + +### Fixed + +* Crash when starting a selection inside the margins. +* Improved font size consistency across multiple monitors with + different DPI ([#47](https://codeberg.org/dnkl/foot/issues/47)). +* Handle trailing comments in `footrc` + + +## 1.4.3 +### Added + +* Section to [README.md](README.md) describing how to programmatically + identify foot. +* [LICENSE](LICENSE), [README.md](README.md) and + [CHANGELOG.md](CHANGELOG.md) are now installed to + `${datadir}/doc/foot`. +* Support for escaping quotes in **pipe-visible** and + **pipe-scrollback** commands. + + +### Changed + +* Primary DA to no longer indicate support for _Selective Erase_, + _Technical Characters_ and _Terminal State Interrogation_. +* Secondary DA to report foot as a VT220 instead of a VT420. +* Secondary DA to report foot's version number in parameter 2, the + _Firmware Version_. The string is made up of foot's major, minor and + patch version numbers, always using two digits for each version + number and without any other separating characters. Thus, _1.4.2_ + would be reported as `010402` (i.e. the full response would be + `\E[>1;010402;0c`). +* Scrollback search to only move the viewport if the match lies + outside it. +* Scrollback search to focus match, that requires a viewport change, + roughly in the center of the screen. +* Extending a selection with the right mouse button now works while + dragging the mouse. + + +### Fixed + +* Crash in scrollback search. +* Crash when a **pipe-visible** or **pipe-scrollback** command + contained an unclosed quote + ([#49](https://codeberg.org/dnkl/foot/issues/49)). + + +### Contributors + +* [birger](https://codeberg.org/birger) +* [cherti](https://codeberg.org/cherti) + + +## 1.4.2 + +### Changed + +* Maximum window title length from 100 to 2048. + + +### Fixed + +* Crash when overwriting a sixel and the row being overwritten did not + cover an entire cell. +* Assertion failure in debug builds when overwriting a sixel image. + + +## 1.4.1 + +### Fixed + +* Compilation errors in release builds with some combinations of + compilers and compiler flags. + + +## 1.4.0 + +### Added + +* `Sync` to terminfo. This is a tmux extension that indicates + _"Synchronized Updates"_ are supported. +* `--hold` command line option to `footclient`. +* Key mapping for `KP_Decimal`. +* Terminfo entries for keypad keys: `ka1`, `ka2`, `ka3`, `kb1`, `kb3`, + `kc1`, `kc2`, `kc3`, `kp5`, `kpADD`, `kpCMA`, `kpDIV`, `kpDOT`, + `kpMUL`, `kpSUB` and `kpZRO`. +* **blink** option to `footrc`; a boolean that lets you control + whether the cursor should blink or not by default. Note that + applications can override this. +* Multi-seat support +* Implemented `C0::FF` (form feed) +* **pipe-visible** and **pipe-scrollback** key bindings. These let you + pipe either the currently visible text, or the entire scrollback to + external tools ([#29](https://codeberg.org/dnkl/foot/issues/29)). Example: + `pipe-visible=[sh -c "xurls | bemenu | xargs -r firefox] Control+Print` + + +### Changed + +* Background transparency to only be used with the default background + color. +* Copy-to-clipboard/primary-selection to insert a line break if either + the last cell on the previous line or the first cell on the next + line is empty. +* Number of lines to scroll is now always clamped to the number of + lines in the scrolling region.. +* New terminal windows spawned with `ctrl`+`shift`+`n` are no longer + double forked. +* Unicode combining character overflow errors are only logged when + debug logging has been enabled. +* OSC 4 (_Set Color_) now updates already rendered cells, excluding + scrollback. +* Mouse cursor from `hand2` to `left_ptr` when client is capturing the + mouse. +* Sixel images are now removed when the font size is **decreased**. +* `DECSCUSR` (_Set Cursor Style_, `CSI Ps SP q`) now uses `Ps=0` + instead of `Ps=2` to reset the style to the user configured default + style. `Ps=2` now always configures a _Steady Block_ cursor. +* `Se` terminfo capability from `\E[2 q` to `\E[ q`. +* Hollow cursor to be drawn when window has lost _keyboard_ focus + rather than _visual_ focus. + + +### Fixed + +* Do not stop an ongoing selection when `shift` is released. When the + client application is capturing the mouse, one must hold down + `shift` to start a selection. This selection is now finalized only + when the mouse button is released - not as soon as `shift` is + released. +* Selected cells did not appear selected if programmatically modified. +* Rare crash when scrolling and the new viewport ended up **exactly** + on the wrap around. +* Selection handling when viewport wrapped around. +* Restore signal mask in the client process. +* Set `IUTF8`. +* Selection of double-width characters. It is no longer possible to + select half of a double-width character. +* Draw hollow block cursor on top of character. +* Set an initial `TIOCSWINSZ`. This ensures clients never read a + `0x0` terminal size ([#20](https://codeberg.org/dnkl/foot/issues/20)). +* Glyphs overflowing into surrounding cells + ([#21](https://codeberg.org/dnkl/foot/issues/21)). +* Crash when last rendered cursor cell had scrolled off screen and + `\E[J3` was executed. +* Assert (debug builds) when an `\e]4` OSC escape was not followed by + a `;`. +* Window title always being set to "foot" on reset. +* Terminfo entry `kb2` (center keypad key); it is now set to `\EOu` + (which is what foot emits) instead of the incorrect value `\EOE`. +* Palette reuse in sixel images. Previously, the palette was reset + after each image. +* Do not auto-resize a sixel image for which the client has specified + a size. This fixes an issue where an image would incorrectly + overflow into the cell row beneath. +* Text printed, or other sixel images drawn, on top of a sixel image + no longer erases the entire image, only the part(s) covered by the + new text or image. +* Sixel images being erased when printing text next to them. +* Sixel handling when resizing window. +* Sixel handling when scrollback wraps around. +* Foot now issues much fewer `wl_surface_damage_buffer()` calls + ([#35](https://codeberg.org/dnkl/foot/issues/35)). +* `C0::VT` to be processed as `C0::LF`. Previously, `C0::VT` would + only move the cursor down, but never scroll. +* `C0::HT` (_Horizontal Tab_, or `\t`) no longer clears `LCF` (_Last + Column Flag_). +* `C0::LF` now always clears `LCF`. Previously, it only cleared it + when the cursor was **not** at the bottom of the scrolling region. +* `IND` and `RI` now clears `LCF`. +* `DECAWM` now clears `LCF`. +* A multi-column character that does not fit on the current line is + now printed on the next line, instead of only printing half the + character. +* Font size can no longer be reduced to negative values + ([#38](https://codeberg.org/dnkl/foot/issues/38)). + + +## 1.3.0 + +### Added + +* User configurable key- and mouse bindings. See `man 5 foot` and the + example `footrc` ([#1](https://codeberg.org/dnkl/foot/issues/1)) +* **initial-window-mode** option to `footrc`, that lets you control + the initial mode for each newly spawned window: _windowed_, + _maximized_ or _fullscreen_. +* **app-id** option to `footrc` and `--app-id` command line option, + that sets the _app-id_ property on the Wayland window. +* **title** option to `footrc` and `--title` command line option, that + sets the initial window title. +* Right mouse button extends the current selection. +* `CSI Ps ; Ps ; Ps t` escape sequences for the following parameters: + `11t`, `13t`, `13;2t`, `14t`, `14;2t`, `15t`, `19t`. +* Unicode combining characters. + + +### Changed + +* Spaces no longer removed from zsh font name completions. +* Default key binding for _spawn-terminal_ to ctrl+shift+n. +* Renderer is now much faster with interactive scrolling + ([#4](https://codeberg.org/dnkl/foot/issues/4)) +* memfd sealing failures are no longer fatal errors. +* Selection to no longer be cleared on resize. +* The current monitor's subpixel order (RGB/BGR/V-RGB/V-BGR) is + preferred over FontConfig's `rgba` property. Only if the monitor's + subpixel order is `unknown` is FontConfig's `rgba` property used. If + the subpixel order is `none`, then grayscale antialiasing is + used. The subpixel order is ignored if antialiasing has been + disabled. +* The four primary font variants (normal, bold, italic, bold italic) + are now loaded in parallel. This speeds up both the initial startup + time, as well as DPI changes. +* Command line parsing no longer tries to parse arguments following + the command-to-execute. This means one can now write `foot sh -c + true` instead of `foot -- sh -c true`. + + +### Removed + +* Keyboard/pointer handler workarounds for Sway 1.2. + + +### Fixed + +* Sixel images moved or deleted on window resize. +* Cursor sometimes incorrectly restored on exit from alternate screen. +* 'Underline' cursor being invisible on underlined text. +* Restored cursor position in 'normal' screen when window was resized + while in 'alt' screen. +* Hostname in OSC 7 URI not being validated. +* OSC 4 with multiple `c;spec` pairs. +* Alt+Return to emit "ESC \r". +* Trackpad sloooow scrolling to eventually scroll a line. +* Memory leak in terminal reset. +* Translation of cursor coordinates on resize +* Scaling color specifiers in OSC sequences. +* `OSC 12 ?` to return the cursor color, not the cursor's text color. +* `OSC 12;#000000` to configure the cursor to use inverted + foreground/background colors. +* Call `ioctl(TIOCSCTTY)` on the pts fd in the slave process. + + +## 1.2.3 + +### Fixed +* Forgot to version bump 1.2.2 + + +## 1.2.2 + +### Changed + +* Changed icon name in `foot.desktop` and `foot-server.desktop` from + _terminal_ to _utilities-terminal_. +* `XDG_SESSION_ID` is now included in the server/daemon default socket + path. + + +### Fixed + +* Window size doubling when moving window between outputs with + different scaling factors ([#3](https://codeberg.org/dnkl/foot/issues/3)). +* Font being too small on monitors with fractional scaling + ([#5](https://codeberg.org/dnkl/foot/issues/5)). + + +## 1.2.1 + +### Fixed + +* Building AUR package + + +## 1.2.0 + +### Added + +* Run-time text resize using ctrl-+, ctrl+- and ctrl+0 +* Font size adjusts dynamically to outputs' DPI +* Reflow text when resizing window +* **pad** option to `footrc` +* **login-shell** option to `footrc` and `--login-shell` command line + option +* Client side decorations (CSDs). This finally makes foot usable on + GNOME. +* Sixel graphics support +* OSC 12 and 112 escape sequences (set/reset text cursor color) +* REP CSI escape sequence +* `oc` to terminfo +* foot-server.desktop file +* Window and cell size reporting escape sequences +* `--hold` command line option +* `--print-pid=FILE|FD` command line option + + +### Changed + +* Subpixel antialiasing is only enabled when background is opaque +* Meta/alt ESC prefix can now be disabled with `\E[?1036l`. In this + mode, the 8:th bit is set and the result is UTF-8 encoded. This can + also be disabled with `\E[1024l` (in which case the Alt key is + effectively being ignored). +* terminfo now uses ST instead of BEL as OSC terminator +* Logging to print to stderr, not stdout +* Backspace now emits DEL (^?), and ctrl+backspace emits BS (^H) + + +### Removed + +* '28' from DA response diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..26ab32a --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,83 @@ +# Foot Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +Participants in the foot community are expected to uphold the described +standards not only in official community spaces (issue trackers, IRC channels, +etc.) but in all public spaces. The Code of Conduct however does acknowledge +that people are fallible and that it is possible to truly correct a past +pattern of unacceptable behavior. That is to say, the scope of the Code of +Conduct does not necessarily extend into the distant past. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior +may be reported to the community leaders responsible for enforcement +at [daniel@ekloef.se](mailto:daniel@ekloef.se). All complaints will +be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +The consequences for Code of Conduct violations will be decided upon and +enforced by community leaders. These may include a formal warning, a temporary +ban from community spaces, a permanent ban from community spaces, etc. + +There are no hard and fast rules for exactly what behavior in which space will +result in what consequences, it is up to the community leaders to enforce the +Code of Conduct in a way that they believe best promotes a healthy community. + +## Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant](https://www.contributor-covenant.org/), +version 2.1, available at +https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..7df8d0b --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,476 @@ +# Installing + +1. [Overview](#overview) +1. [Requirements](#requirements) + 1. [Running](#running) + 1. [Building](#building) +1. [Other](#other) + 1. [Setup](#setup) + 1. [Options](#options) + 1. [Release build](#release-build) + 1. [Size optimized](#size-optimized) + 1. [Performance optimized, non-PGO](#performance-optimized-non-pgo) + 1. [Performance optimized, PGO](#performance-optimized-pgo) + 1. [Partial PGO](#partial-pgo) + 1. [Full PGO](#full-pgo) + 1. [Use the generated PGO data](#use-the-generated-pgo-data) + 1. [Profile Guided Optimization](#profile-guided-optimization) + 1. [Debug build](#debug-build) + 1. [Terminfo](#terminfo) + 1. [Running the new build](#running-the-new-build) + + +## Overview + +foot makes use of a couple of libraries I have developed: +[tllist](https://codeberg.org/dnkl/tllist) and +[fcft](https://codeberg.org/dnkl/fcft). As such, they will most likely +not have been installed already. You can either install them as system +libraries or build them as _subprojects_ in foot. + +When building foot, they will first be searched for as system +libraries. If **found**, foot will link dynamically against them. +If **not** found, meson will attempt to download and build them as +subprojects. + + +## Requirements + +### Running + +* UTF-8 locale +* fontconfig +* freetype +* pixman +* wayland (_client_ and _cursor_ libraries) +* xkbcommon +* utf8proc (_optional_, needed for grapheme clustering) +* libutempter (_optional_, needed for utmp logging on Linux) +* ulog (_optional_, needed for utmp logging on FreeBSD) +* [fcft](https://codeberg.org/dnkl/fcft) [^1] + +[^1]: can also be built as subprojects, in which case they are + statically linked. + +If you are packaging foot, you may also want to consider adding the +following **optional** dependencies: + +* libnotify: desktop notifications by default uses `notify-send`. +* xdg-utils: URLs are by default launched with `xdg-open`. +* bash-completion: If you want completion for positional arguments. + +### Building + +In addition to the dev variant of the packages above, you need: + +* meson +* ninja +* wayland protocols +* ncurses (needed to generate terminfo) +* scdoc (for man page generation, not needed if documentation is disabled) +* llvm (for PGO builds with Clang) +* [tllist](https://codeberg.org/dnkl/tllist) [^1] +* systemd (optional, foot will install systemd unit files if detected) + +A note on compilers; in general, foot runs **much** faster when +compiled with gcc instead of clang. A profile-guided gcc build can be +more than twice as fast as a clang build. + +**Note** GCC 10.1 has a performance regression that severely affects +foot when doing PGO builds and building with `-O2`; it is about 30-40% +slower compared to GCC 9.3. + +The work around is simple: make sure you build with `-O3`. This is the +default with `meson --buildtype=release`, but e.g. `makepkg` can +override it (`makepkg` uses `-O2` by default). + +## Other + +Foot uses _meson_. If you are unfamiliar with it, the official +[tutorial](https://mesonbuild.com/Tutorial.html) might be a good +starting point. + +A note on terminfo; the terminfo database exposes terminal +capabilities to the applications running inside the terminal. As such, +it is important that the terminfo used reflects the actual +terminal. Using the `xterm-256color` terminfo will, in many cases, +work, but I still recommend using foot's own terminfo. There are two +reasons for this: + +* foot's terminfo contains a couple of non-standard capabilities, + used by e.g. tmux. +* New capabilities added to the `xterm-256color` terminfo could + potentially break foot. +* There may be future additions or changes to foot's terminfo. + +As of ncurses 2021-07-31, ncurses includes a version of foot's +terminfo. **The recommendation is to use those**, and only install the +terminfo definitions from this git repo if the system's ncurses +predates 2021-07-31. + +But, note that the foot terminfo definitions in ncurses' lack the +non-standard capabilities. This mostly affects tmux; without them, +`terminal-overrides` must be configured to enable truecolor +support. For this reason, it _is_ possible to install "our" terminfo +definitions as well, either in a non-default location, or under a +different name. + +Both have their set of issues. When installing to a non-default +location, foot will set the environment variable `TERMINFO` in the +child process. However, there are many situations where this simply +does not work. See https://codeberg.org/dnkl/foot/issues/695 for +details. + +Installing them under a different name generally works well, but will +break applications that check if `$TERM == foot`. + +Hence the recommendation to simply use ncurses' terminfo definitions +if available. + +If packaging "our" terminfo definitions, I recommend doing that as a +separate package, to allow them to be installed on remote systems +without having to install foot itself. + + +### Setup + +To build, first, create a build directory, and switch to it: +```sh +mkdir -p bld/release && cd bld/release +``` + +### Options + +Available compile-time options: + +| Option | Type | Default | Description | Extra dependencies | +|--------------------------------------|---------|-------------------------|---------------------------------------------------------------------------------|---------------------| +| `-Ddocs` | feature | `auto` | Builds and install documentation | scdoc | +| `-Dtests` | bool | `true` | Build tests (adds a `ninja test` build target) | None | +| `-Dime` | bool | `true` | Enables IME support | None | +| `-Dgrapheme-clustering` | feature | `auto` | Enables grapheme clustering | libutf8proc | +| `-Dterminfo` | feature | `enabled` | Build and install terminfo files | tic (ncurses) | +| `-Ddefault-terminfo` | string | `foot` | Default value of `TERM` | None | +| `-Dterminfo-base-name` | string | `-Ddefault-terminfo` | Base name of the generated terminfo files | None | +| `-Dcustom-terminfo-install-location` | string | `${datadir}/terminfo` | Value to set `TERMINFO` to | None | +| `-Dsystemd-units-dir` | string | `${systemduserunitdir}` | Where to install the systemd service files (absolute) | None | +| `-Dutmp-backend` | combo | `auto` | Which utmp backend to use (`none`, `libutempter`, `ulog` or `auto`) | libutempter or ulog | +| `-Dutmp-default-helper-path` | string | `auto` | Default path to utmp helper binary. `auto` selects path based on `utmp-backend` | None | + +Documentation includes the man pages, readme, changelog and license +files. + +`-Ddefault-terminfo`: I strongly recommend leaving the default +value. Use this option if you plan on installing the terminfo files +under a different name. Setting this changes the default value of +`$TERM`, and the names of the terminfo files (if +`-Dterminfo=enabled`). + +If you want foot to use the terminfo files from ncurses, but still +package foot's own terminfo files under a different name, you can use +the `-Dterminfo-base-name` option. Many distributions use the name +`foot-extra`, and thus it might be a good idea to reuse that: + +```sh +meson ... -Ddefault-terminfo=foot -Dterminfo-base-name=foot-extra +``` +(or just leave out `-Ddefault-terminfo`, since it defaults to `foot` anyway). + +Finally, `-Dcustom-terminfo-install-location` enables foot's terminfo +to co-exist with ncurses' version, without changing the terminfo +names. The idea is that you install foot's terminfo to a non-standard +location, for example `/usr/share/foot/terminfo`. Use +`-Dcustom-terminfo-install-location` to tell foot where the terminfo +is. Foot will set the environment variable `TERMINFO` to this value +(with `${prefix}` added). The value is **relative to ${prefix}**. + +Note that there are several issues with this approach: +https://codeberg.org/dnkl/foot/issues/695. + +If left unset, foot will **not** set or modify `TERMINFO`. + +`-Dterminfo` can be used to disable building the terminfo definitions +in the meson build. It does **not** change the default value of +`TERM`, and it does **not** disable `TERMINFO`, if +`-Dcustom-terminfo-install-location` has been set. Use this if +packaging the terminfo definitions in a separate package (and the +build script isn't shared with the 'foot' package). + +Example: + +```sh +meson --prefix=/usr -Dcustom-terminfo-install-location=lib/foot/terminfo +``` + +The above tells foot its terminfo definitions will be installed to +`/usr/lib/foot/terminfo`. This is the value foot will set the +`TERMINFO` environment variable to. + +If `-Dterminfo` is enabled (the default), then the terminfo files will +be built as part of the regular build process, and installed to the +specified location. + +Packagers may want to set `-Dterminfo=disabled`, and manually build +and [install the terminfo](#terminfo) files instead. + + +### Release build + +Below are instructions for building foot either [size +optimized](#size-optimized), [performance +optimized](performance-optimized-non-pgo), or performance +optimized using [PGO](#performance-optimized-pgo). + +PGO - _Profile Guided Optimization_ - is a way to optimize a program +better than `-O3` can, and is done by compiling foot twice: first to +generate an instrumented version which is used to run a payload that +exercises the performance critical parts of foot, and then a second +time to rebuild foot using the generated profiling data to guide +optimization. + +In addition to being faster, PGO builds also tend to be smaller than +regular `-O3` builds. + + +#### Size optimized + +To optimize for size (i.e. produce a small binary): + +```sh +export CFLAGS="$CFLAGS -Os" +meson --buildtype=release --prefix=/usr -Db_lto=true ../.. +ninja +ninja test +ninja install +``` + +#### Performance optimized, non-PGO + +To do a regular, non-PGO build optimized for performance: + +```sh +export CFLAGS="$CFLAGS -O3" +meson --buildtype=release --prefix=/usr -Db_lto=true ../.. +ninja +ninja test +ninja install +``` + +Use `-O2` instead of `-O3` if you prefer a slightly smaller (and +slower!) binary. + + +#### Performance optimized, PGO + +There are a lot more steps involved in a PGO build, and for this +reason there are a number of helper scripts available. + +`pgo/pgo.sh` is a standalone script that pieces together the other +scripts in the `pgo` directory to do a complete PGO build. This script +is intended to be used when doing manual builds. + +Note that all "full" PGO builds (which `auto` will prefer, if +possible) **require** `LC_CTYPE` to be set to an UTF-8 locale. This is +**not** done automatically. + +Example: + +```sh +cd foot +./pgo/pgo.sh auto . /tmp/foot-pgo-build-output +``` + +(run `./pgo/pgo.sh` to get help on usage) + +It supports a couple of different PGO builds; partial (covered in +detail below), full (also covered in detail below), and (full) +headless builds using Sway or cage. + +Packagers may want to use it as inspiration, but may choose to support +only a specific build type; e.g. full/headless with Sway. + +To do a manual PGO build, instead of using the script(s) mentioned +above, detailed instructions follows: + +First, configure the build directory: + +```sh +export CFLAGS="$CFLAGS -O3" +meson --buildtype=release --prefix=/usr -Db_lto=true ../.. +``` + +It is **very** important `-O3` is being used here, as GCC-10.1.x and +later have a regression where PGO with `-O2` is **much** slower. + +Clang users **must** add `-Wno-ignored-optimization-argument` to +`CFLAGS`. + +Then, tell meson we want to _generate_ profiling data, and build: + +```sh +meson configure -Db_pgo=generate +ninja +ninja test +``` + +Next, we need to actually generate the profiling data. + +There are two ways to do this: a [partial PGO build using a PGO +helper](#partial-pgo) binary, or a [full PGO build](#full-pgo) by +running the real foot binary. The latter has slightly better results +(i.e. results in a faster binary), but must be run in a Wayland +session. + +A full PGO build also tends to be smaller than a partial build. + + +##### Partial PGO + +This method uses a PGO helper binary that links against the VT parser +only. It is similar to a mock test; it instantiates a dummy terminal +instance and then directly calls the VT parser with stimuli. + +It explicitly does **not** include the Wayland backend and as such, it +does not require a running Wayland session. The downside is that not +all code paths in foot is exercised. In particular, the **rendering** +code is not. As a result, the final binary built using this method is +slightly slower than when doing a [full PGO](#full-pgo) build. + +We will use the `pgo` binary along with input corpus generated by +`scripts/generate-alt-random-writes.py`: + +```sh +./utils/xtgettcap +./footclient --version +./foot --version +tmp_file=$(mktemp) +../../scripts/generate-alt-random-writes \ + --rows=67 \ + --cols=135 \ + --scroll \ + --scroll-region \ + --colors-regular \ + --colors-bright \ + --colors-256 \ + --colors-rgb \ + --attr-bold \ + --attr-italic \ + --attr-underline \ + --sixel \ + ${tmp_file} +./pgo ${tmp_file} ${tmp_file} ${tmp_file} +rm ${tmp_file} +``` + +The first step, running `./foot --version` and `./footclient +--version` etc, might seem unnecessary, but is needed to ensure we +have _some_ profiling data for functions not covered by the PGO helper +binary, for **all** binaries. Without this, the final link phase will +fail. + +The snippet above then creates an (empty) temporary file. Then, it +runs a script that generates random escape sequences (if you cat +`${tmp_file}` in a terminal, you'll see random colored characters all +over the screen). Finally, we feed the randomly generated escape +sequences to the PGO helper. This is what generates the profiling data +used in the next step. + +You are now ready to [use the generated PGO +data](#use-the-generated-pgo-data). + + +##### Full PGO + +This method requires a running Wayland session. + +We will use the script `scripts/generate-alt-random-writes.py`: + +```sh +./utils/xtgettcap +./footclient --version +foot_tmp_file=$(mktemp) +./foot \ + --config=/dev/null \ + --override tweak.grapheme-shaping=no \ + --term=xterm \ + sh -c " --scroll --scroll-region --colors-regular --colors-bright --colors-256 --colors-rgb --attr-bold --attr-italic --attr-underline --sixel ${foot_tmp_file} && cat ${foot_tmp_file}" +rm ${foot_tmp_file} +``` + +You should see a foot window open up, with random colored text. The +window should close after ~1-2s. + +The first step, `./utils/xtgettcap && ./footclient --version` +might seem unnecessary, but is needed to ensure we have _some_ +profiling data for **all** binaries we build. Without this, the final +link phase will fail. + + +##### Use the generated PGO data + +Now that we have _generated_ PGO data, we need to rebuild foot. This +time telling meson (and ultimately gcc/clang) to _use_ the PGO data. + +If using Clang, now do (this requires _llvm_ to have been installed): + +```sh +llvm-profdata merge default_*profraw --output=default.profdata +``` + +Next, tell meson to _use_ the profile data we just generated, and rebuild: + +```sh +meson configure -Db_pgo=use +ninja +ninja test +``` + +Continue reading in [Running the new build](#running-the-new-build) + + +### Debug build + +```sh +meson --buildtype=debug ../.. +ninja +ninja test +``` + +### Terminfo + +By default, building foot also builds the terminfo files. If packaging +the terminfo files in a separate package, it might be easier to simply +disable the terminfo files in the regular build, and compile the +terminfo files manually instead. + +To build the terminfo files, run: + +```sh +sed 's/@default_terminfo@/foot/g' foot.info | \ + tic -o -x -e foot,foot-direct - +``` + +Where _"output-directory"_ **must** match the value passed to +`-Dcustom-terminfo-install-location` in the foot build. If +`-Dcustom-terminfo-install-location` has not been set, `-o +` can simply be omitted. + +Or, if packaging: + +```sh +tic -o ${DESTDIR}/usr/share/terminfo ... +``` + + +### Running the new build + +You can now run it directly from the build directory: +```sh +./foot +``` + +Or, if you did not install the terminfo definitions: + +```sh +./foot --term xterm-256color +``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a915c5b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Daniel Eklöf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d0c775a --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +toes diff --git a/async.c b/async.c new file mode 100644 index 0000000..3ad2a8c --- /dev/null +++ b/async.c @@ -0,0 +1,35 @@ +#include "async.h" + +#include +#include +#include + +#define LOG_MODULE "async" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +enum async_write_status +async_write(int fd, const void *_data, size_t len, size_t *idx) +{ + const uint8_t *const data = _data; + size_t left = len - *idx; + + while (left > 0) { + ssize_t ret = write(fd, &data[*idx], left); + + if (ret < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + return ASYNC_WRITE_REMAIN; + + return ASYNC_WRITE_ERR; + } + + LOG_DBG("wrote %zd bytes of %zu (%zu left) to FD=%d", + ret, left, left - ret, fd); + + *idx += ret; + left -= ret; + } + + return ASYNC_WRITE_DONE; +} diff --git a/async.h b/async.h new file mode 100644 index 0000000..876368d --- /dev/null +++ b/async.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +enum async_write_status {ASYNC_WRITE_DONE, ASYNC_WRITE_REMAIN, ASYNC_WRITE_ERR}; + +/* + * Primitive that writes data to a NONBLOCK:ing FD. + * + * _data: points to the beginning of the buffer + * len: total size of the data buffer + * idx: pointer to byte offset into data buffer - writing starts here. + * + * Thus, the total amount of data to write is (len - *idx). *idx is + * updated such that it points to the next unwritten byte in the data + * buffer. + * + * I.e. if the return value is: + * - ASYNC_WRITE_DONE, then the *idx == len. + * - ASYNC_WRITE_REMAIN, then *idx < len + * - ASYNC_WRITE_ERR, there was an error, and no data was written + */ +enum async_write_status async_write( + int fd, const void *data, size_t len, size_t *idx); diff --git a/base64.c b/base64.c new file mode 100644 index 0000000..db697cb --- /dev/null +++ b/base64.c @@ -0,0 +1,172 @@ +#include "base64.h" + +#include +#include +#include +#include +#include + +#define LOG_MODULE "base64" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" + +enum { + P = 1 << 6, // Padding byte (=) + I = 1 << 7, // Invalid byte ([^A-Za-z0-9+/=]) +}; + +static const uint8_t reverse_lookup[256] = { + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, 62, I, I, I, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, I, I, I, P, I, I, + I, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, I, I, I, I, I, + I, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, + I, I, I, I, I, I, I, I, I, I, I, I, I, I, I, I +}; + +static const char lookup[64] = { + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/" +}; + +char * +base64_decode(const char *s, size_t *size) +{ + const size_t len = strlen(s); + if (unlikely(len % 4 != 0)) { + errno = EINVAL; + return NULL; + } + + char *ret = malloc(len / 4 * 3 + 1); + if (unlikely(ret == NULL)) + return NULL; + + if (unlikely(size != NULL)) + *size = len / 4 * 3; + + for (size_t i = 0, o = 0; i < len; i += 4, o += 3) { + unsigned a = reverse_lookup[(unsigned char)s[i + 0]]; + unsigned b = reverse_lookup[(unsigned char)s[i + 1]]; + unsigned c = reverse_lookup[(unsigned char)s[i + 2]]; + unsigned d = reverse_lookup[(unsigned char)s[i + 3]]; + + unsigned u = a | b | c | d; + if (unlikely(u & I)) + goto invalid; + + if (unlikely(u & P)) { + if (unlikely(i + 4 != len || (a | b) & P || (c & P && !(d & P)))) + goto invalid; + + if (unlikely(size != NULL)) { + if (c & P) + *size = len / 4 * 3 - 2; + else + *size = len / 4 * 3 - 1; + } + + c &= 63; + d &= 63; + } + + uint32_t v = a << 18 | b << 12 | c << 6 | d << 0; + char x = (v >> 16) & 0xff; + char y = (v >> 8) & 0xff; + char z = (v >> 0) & 0xff; + + LOG_DBG("%c%c%c", x, y, z); + ret[o + 0] = x; + ret[o + 1] = y; + ret[o + 2] = z; + } + + ret[len / 4 * 3] = '\0'; + return ret; + +invalid: + free(ret); + errno = EINVAL; + return NULL; +} + +char * +base64_encode(const uint8_t *data, size_t size) +{ + xassert(size % 3 == 0); + if (unlikely(size % 3 != 0)) + return NULL; + + char *ret = malloc(size / 3 * 4 + 1); + if (unlikely(ret == NULL)) + return NULL; + + for (size_t i = 0, o = 0; i < size; i += 3, o += 4) { + int x = data[i + 0]; + int y = data[i + 1]; + int z = data[i + 2]; + + uint32_t v = x << 16 | y << 8 | z << 0; + + unsigned a = (v >> 18) & 0x3f; + unsigned b = (v >> 12) & 0x3f; + unsigned c = (v >> 6) & 0x3f; + unsigned d = (v >> 0) & 0x3f; + + char c0 = lookup[a]; + char c1 = lookup[b]; + char c2 = lookup[c]; + char c3 = lookup[d]; + + ret[o + 0] = c0; + ret[o + 1] = c1; + ret[o + 2] = c2; + ret[o + 3] = c3; + + LOG_DBG("base64: encode: %c%c%c%c", c0, c1, c2, c3); + } + + ret[size / 3 * 4] = '\0'; + return ret; +} + +void +base64_encode_final(const uint8_t *data, size_t size, char result[4]) +{ + xassert(size > 0); + xassert(size < 3); + + uint32_t v = 0; + if (size >= 1) + v |= data[0] << 16; + if (size >= 2) + v |= data[1] << 8; + + unsigned a = (v >> 18) & 0x3f; + unsigned b = (v >> 12) & 0x3f; + unsigned c = (v >> 6) & 0x3f; + + char c0 = lookup[a]; + char c1 = lookup[b]; + char c2 = size == 2 ? lookup[c] : '='; + char c3 = '='; + + result[0] = c0; + result[1] = c1; + result[2] = c2; + result[3] = c3; + + LOG_DBG("base64: encode: %c%c%c%c", c0, c1, c2, c3); +} diff --git a/base64.h b/base64.h new file mode 100644 index 0000000..3fa3d07 --- /dev/null +++ b/base64.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +char *base64_decode(const char *s, size_t *out_len); +char *base64_encode(const uint8_t *data, size_t size); +void base64_encode_final(const uint8_t *data, size_t size, char result[4]); diff --git a/box-drawing.c b/box-drawing.c new file mode 100644 index 0000000..e69d964 --- /dev/null +++ b/box-drawing.c @@ -0,0 +1,3450 @@ +#include "box-drawing.h" + +#include +#include +#include + +#define LOG_MODULE "box-drawing" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "config.h" +#include "macros.h" +#include "stride.h" +#include "terminal.h" +#include "util.h" +#include "xmalloc.h" + +#define clamp(x, lower, upper) (min(upper, max(x, lower))) + +enum thickness { + LIGHT, + HEAVY, +}; + +struct buf { + uint8_t *data; + pixman_image_t *pix; + pixman_format_code_t format; + int width; + int height; + int stride; + bool solid_shades; + + int thickness[2]; + + /* For octants, sextants and wedges */ + int x_halfs[2]; + int y_thirds[2]; + + /* For octants */ + int y_quads[3]; +}; + +static const pixman_color_t white = {0xffff, 0xffff, 0xffff, 0xffff}; + +static void +change_buffer_format(struct buf *buf, pixman_format_code_t new_format) +{ + int stride = stride_for_format_and_width(new_format, buf->width); + uint8_t *new_data = xcalloc(buf->height * stride, 1); + pixman_image_t *new_pix = pixman_image_create_bits_no_clear( + new_format, buf->width, buf->height, (uint32_t *)new_data, stride); + + if (new_pix == NULL) { + errno = ENOMEM; + perror(__func__); + abort(); + } + + pixman_image_unref(buf->pix); + free(buf->data); + + buf->data = new_data; + buf->pix = new_pix; + buf->format = new_format; + buf->stride = stride; +} + +static int NOINLINE +_thickness(int base_thickness, enum thickness thick) +{ + int multiplier = thick * 2 + 1; + + xassert(base_thickness >= 1); + xassert((thick == LIGHT && multiplier == 1) || + (thick == HEAVY && multiplier == 3)); + + return base_thickness * multiplier; +} +#define thickness(thick) buf->thickness[thick] + +static void NOINLINE +_hline(struct buf *buf, int x1, int x2, int y, int thick) +{ + pixman_box32_t box = { + .x1 = min(max(x1, 0), buf->width), + .x2 = min(max(x2, 0), buf->width), + .y1 = min(max(y, 0), buf->height), + .y2 = min(max(y + thick, 0), buf->height), + }; + pixman_image_fill_boxes(PIXMAN_OP_SRC, buf->pix, &white, 1, &box); +} + +#define hline(x1, x2, y, thick) _hline(buf, x1, x2, y, thick) + +static void NOINLINE +_vline(struct buf *buf, int y1, int y2, int x, int thick) +{ + pixman_box32_t box = { + .x1 = min(max(x, 0), buf->width), + .x2 = min(max(x + thick, 0), buf->width), + .y1 = min(max(y1, 0), buf->height), + .y2 = min(max(y2, 0), buf->height), + }; + pixman_image_fill_boxes(PIXMAN_OP_SRC, buf->pix, &white, 1, &box); +} + +#define vline(y1, y2, x, thick) _vline(buf, y1, y2, x, thick) + +static void NOINLINE +_rect(struct buf *buf, int x1, int y1, int x2, int y2) +{ + pixman_box32_t box = { + .x1 = min(max(x1, 0), buf->width), + .y1 = min(max(y1, 0), buf->height), + .x2 = min(max(x2, 0), buf->width), + .y2 = min(max(y2, 0), buf->height), + }; + pixman_image_fill_boxes(PIXMAN_OP_SRC, buf->pix, &white, 1, &box); +} + +#define rect(x1, y1, x2, y2) _rect(buf, x1, y1, x2, y2) + +static void NOINLINE +_hline_middle(struct buf *buf, enum thickness _thick) +{ + int thick = thickness(_thick); + hline(0, buf->width, (buf->height - thick) / 2, thick); +} + +static void NOINLINE +_hline_middle_left(struct buf *buf, enum thickness _vthick, enum thickness _hthick) +{ + int vthick = thickness(_vthick); + int hthick = thickness(_hthick); + _hline(buf, 0, (buf->width + vthick) / 2, (buf->height - hthick) / 2, hthick); +} + +static void NOINLINE +_hline_middle_right(struct buf *buf, enum thickness _vthick, enum thickness _hthick) +{ + int vthick = thickness(_vthick); + int hthick = thickness(_hthick); + hline((buf->width - vthick) / 2, buf->width, (buf->height - hthick) / 2, hthick); +} + +static void NOINLINE +_vline_middle(struct buf *buf, enum thickness _thick) +{ + int thick = thickness(_thick); + vline(0, buf->height, (buf->width - thick) / 2, thick); +} + +static void NOINLINE +_vline_middle_up(struct buf *buf, enum thickness _vthick, enum thickness _hthick) +{ + int vthick = thickness(_vthick); + int hthick = thickness(_hthick); + vline(0, (buf->height + hthick) / 2, (buf->width - vthick) / 2, vthick); +} + +static void NOINLINE +_vline_middle_down(struct buf *buf, enum thickness _vthick, enum thickness _hthick) +{ + int vthick = thickness(_vthick); + int hthick = thickness(_hthick); + vline((buf->height - hthick) / 2, buf->height, (buf->width - vthick) / 2, vthick); +} + +#define hline_middle(thick) _hline_middle(buf, thick) +#define hline_middle_left(thick) _hline_middle_left(buf, thick, thick) +#define hline_middle_right(thick) _hline_middle_right(buf, thick, thick) +#define hline_middle_left_mixed(vthick, hthick) _hline_middle_left(buf, vthick, hthick) +#define hline_middle_right_mixed(vthick, hthick) _hline_middle_right(buf, vthick, hthick) +#define vline_middle(thick) _vline_middle(buf, thick) +#define vline_middle_up(thick) _vline_middle_up(buf, thick, thick) +#define vline_middle_down(thick) _vline_middle_down(buf, thick, thick) +#define vline_middle_up_mixed(vthick, hthick) _vline_middle_up(buf, vthick, hthick) +#define vline_middle_down_mixed(vthick, hthick) _vline_middle_down(buf, vthick, hthick) + +static void +draw_box_drawings_light_horizontal(struct buf *buf) +{ + hline_middle(LIGHT); +} + +static void +draw_box_drawings_heavy_horizontal(struct buf *buf) +{ + hline_middle(HEAVY); +} + +static void +draw_box_drawings_light_vertical(struct buf *buf) +{ + vline_middle(LIGHT); +} + +static void +draw_box_drawings_heavy_vertical(struct buf *buf) +{ + vline_middle(HEAVY); +} + +static void +draw_box_drawings_dash_horizontal(struct buf *buf, int count, int thick, int gap) +{ + int width = buf->width; + int height = buf->height; + + xassert(count >= 2 && count <= 4); + const int gap_count = count - 1; + + int dash_width = (width - (gap_count * gap)) / count; + while (dash_width <= 0 && gap > 1) { + gap--; + dash_width = (width - (gap_count * gap)) / count; + } + + if (dash_width <= 0) { + hline_middle(LIGHT); + return; + } + + xassert(count * dash_width + gap_count * gap <= width); + + int remaining = width - count * dash_width - gap_count * gap; + + int x[4] = {0}; + int w[4] = {dash_width, dash_width, dash_width, dash_width}; + + x[0] = 0; + + x[1] = x[0] + w[0] + gap; + if (count == 2) + w[1] = width - x[1]; + else if (count == 3) + w[1] += remaining; + else + w[1] += remaining / 2; + + if (count >= 3) { + x[2] = x[1] + w[1] + gap; + if (count == 3) + w[2] = width - x[2]; + else + w[2] += remaining - remaining / 2; + } + + if (count >= 4) { + x[3] = x[2] + w[2] + gap; + w[3] = width - x[3]; + } + + hline(x[0], x[0] + w[0], (height - thick) / 2, thick); + hline(x[1], x[1] + w[1], (height - thick) / 2, thick); + if (count >= 3) + hline(x[2], x[2] + w[2], (height - thick) / 2, thick); + if (count >= 4) + hline(x[3], x[3] + w[3], (height - thick) / 2, thick); +} + +static void +draw_box_drawings_dash_vertical(struct buf *buf, int count, int thick, int gap) +{ + int width = buf->width; + int height = buf->height; + + xassert(count >= 2 && count <= 4); + const int gap_count = count - 1; + + int dash_height = (height - (gap_count * gap)) / count; + while (dash_height <= 0 && gap > 1) { + gap--; + dash_height = (height - (gap_count * gap)) / count; + } + + if (dash_height <= 0) { + vline_middle(LIGHT); + return; + } + + xassert(count * dash_height + gap_count * gap <= height); + + int remaining = height - count * dash_height - gap_count * gap; + + int y[4] = {0}; + int h[4] = {dash_height, dash_height, dash_height, dash_height}; + + y[0] = 0; + + y[1] = y[0] + h[0] + gap; + if (count == 2) + h[1] = height - y[1]; + else if (count == 3) + h[1] += remaining; + else + h[1] += remaining / 2; + + if (count >= 3) { + y[2] = y[1] + h[1] + gap; + if (count == 3) + h[2] = height - y[2]; + else + h[2] += remaining - remaining / 2; + } + + if (count >= 4) { + y[3] = y[2] + h[2] + gap; + h[3] = height - y[3]; + } + + vline(y[0], y[0] + h[0], (width - thick) / 2, thick); + vline(y[1], y[1] + h[1], (width - thick) / 2, thick); + if (count >= 3) + vline(y[2], y[2] + h[2], (width - thick) / 2, thick); + if (count >= 4) + vline(y[3], y[3] + h[3], (width - thick) / 2, thick); +} + +static void +draw_box_drawings_light_triple_dash_horizontal(struct buf *buf) +{ + draw_box_drawings_dash_horizontal(buf, 3, thickness(LIGHT), thickness(LIGHT)); +} + +static void +draw_box_drawings_heavy_triple_dash_horizontal(struct buf *buf) +{ + draw_box_drawings_dash_horizontal(buf, 3, thickness(HEAVY), thickness(LIGHT)); +} + +static void +draw_box_drawings_light_triple_dash_vertical(struct buf *buf) +{ + draw_box_drawings_dash_vertical(buf, 3, thickness(LIGHT), thickness(HEAVY)); +} + +static void +draw_box_drawings_heavy_triple_dash_vertical(struct buf *buf) +{ + draw_box_drawings_dash_vertical(buf, 3, thickness(HEAVY), thickness(HEAVY)); +} + +static void +draw_box_drawings_light_quadruple_dash_horizontal(struct buf *buf) +{ + draw_box_drawings_dash_horizontal(buf, 4, thickness(LIGHT), thickness(LIGHT)); +} + +static void +draw_box_drawings_heavy_quadruple_dash_horizontal(struct buf *buf) +{ + draw_box_drawings_dash_horizontal(buf, 4, thickness(HEAVY), thickness(LIGHT)); +} + +static void +draw_box_drawings_light_quadruple_dash_vertical(struct buf *buf) +{ + draw_box_drawings_dash_vertical(buf, 4, thickness(LIGHT), thickness(LIGHT)); +} + +static void +draw_box_drawings_heavy_quadruple_dash_vertical(struct buf *buf) +{ + draw_box_drawings_dash_vertical(buf, 4, thickness(HEAVY), thickness(LIGHT)); +} + +static void +draw_box_drawings_light_down_and_right(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_light_and_right_heavy(struct buf *buf) +{ + hline_middle_right_mixed(LIGHT, HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_heavy_and_right_light(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle_down_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_heavy_down_and_right(struct buf *buf) +{ + hline_middle_right(HEAVY); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_light_down_and_left(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_light_and_left_heavy(struct buf *buf) +{ + hline_middle_left_mixed(LIGHT, HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_heavy_and_left_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle_down_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_heavy_down_and_left(struct buf *buf) +{ + hline_middle_left(HEAVY); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_light_up_and_right(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_up_light_and_right_heavy(struct buf *buf) +{ + hline_middle_right_mixed(LIGHT, HEAVY); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_up_heavy_and_right_light(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle_up_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_heavy_up_and_right(struct buf *buf) +{ + hline_middle_right(HEAVY); + vline_middle_up(HEAVY); +} + +static void +draw_box_drawings_light_up_and_left(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_up_light_and_left_heavy(struct buf *buf) +{ + hline_middle_left_mixed(LIGHT, HEAVY); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_up_heavy_and_left_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle_up_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_heavy_up_and_left(struct buf *buf) +{ + hline_middle_left(HEAVY); + vline_middle_up(HEAVY); +} + +static void +draw_box_drawings_light_vertical_and_right(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_vertical_light_and_right_heavy(struct buf *buf) +{ + hline_middle_right_mixed(LIGHT, HEAVY); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_up_heavy_and_right_down_light(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle_up_mixed(HEAVY, LIGHT); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_heavy_and_right_up_light(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle_up(LIGHT); + vline_middle_down_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_vertical_heavy_and_right_light(struct buf *buf) +{ + hline_middle_right(LIGHT); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_down_light_and_right_up_heavy(struct buf *buf) +{ + hline_middle_right(HEAVY); + vline_middle_up(HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_up_light_and_right_down_heavy(struct buf *buf) +{ + hline_middle_right(HEAVY); + vline_middle_up(LIGHT); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_heavy_vertical_and_right(struct buf *buf) +{ + hline_middle_right(HEAVY); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_light_vertical_and_left(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_vertical_light_and_left_heavy(struct buf *buf) +{ + hline_middle_left_mixed(LIGHT, HEAVY); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_up_heavy_and_left_down_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle_up_mixed(HEAVY, LIGHT); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_heavy_and_left_up_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle_up(LIGHT); + vline_middle_down_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_vertical_heavy_and_left_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_down_light_and_left_up_heavy(struct buf *buf) +{ + hline_middle_left(HEAVY); + vline_middle_up(HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_up_light_and_left_down_heavy(struct buf *buf) +{ + hline_middle_left(HEAVY); + vline_middle_up(LIGHT); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_heavy_vertical_and_left(struct buf *buf) +{ + hline_middle_left(HEAVY); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_light_down_and_horizontal(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_left_heavy_and_right_down_light(struct buf *buf) +{ + hline_middle_left_mixed(LIGHT, HEAVY); + hline_middle_right(LIGHT); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_right_heavy_and_left_down_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right_mixed(LIGHT, HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_light_and_horizontal_heavy(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_heavy_and_horizontal_light(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle_down_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_right_light_and_left_down_heavy(struct buf *buf) +{ + hline_middle_left(HEAVY); + hline_middle_right(LIGHT); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_left_light_and_right_down_heavy(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right(HEAVY); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_heavy_down_and_horizontal(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_light_up_and_horizontal(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_left_heavy_and_right_up_light(struct buf *buf) +{ + hline_middle_left_mixed(LIGHT, HEAVY); + hline_middle_right(LIGHT); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_right_heavy_and_left_up_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right_mixed(LIGHT, HEAVY); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_up_light_and_horizontal_heavy(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_up_heavy_and_horizontal_light(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle_up_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_right_light_and_left_up_heavy(struct buf *buf) +{ + hline_middle_left(HEAVY); + hline_middle_right(LIGHT); + vline_middle_up(HEAVY); +} + +static void +draw_box_drawings_left_light_and_right_up_heavy(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right(HEAVY); + vline_middle_up(HEAVY); +} + +static void +draw_box_drawings_heavy_up_and_horizontal(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle_up(HEAVY); +} + +static void +draw_box_drawings_light_vertical_and_horizontal(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_left_heavy_and_right_vertical_light(struct buf *buf) +{ + hline_middle_left_mixed(LIGHT, HEAVY); + hline_middle_right(LIGHT); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_right_heavy_and_left_vertical_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right_mixed(LIGHT, HEAVY); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_vertical_light_and_horizontal_heavy(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle(LIGHT); +} + +static void +draw_box_drawings_up_heavy_and_down_horizontal_light(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle_up_mixed(HEAVY, LIGHT); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_down_heavy_and_up_horizontal_light(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle_up(LIGHT); + vline_middle_down_mixed(HEAVY, LIGHT); +} + +static void +draw_box_drawings_vertical_heavy_and_horizontal_light(struct buf *buf) +{ + hline_middle(LIGHT); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_left_up_heavy_and_right_down_light(struct buf *buf) +{ + hline_middle_left(HEAVY); + hline_middle_right(LIGHT); + vline_middle_up(HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_right_up_heavy_and_left_down_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right(HEAVY); + vline_middle_up(HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_left_down_heavy_and_right_up_light(struct buf *buf) +{ + hline_middle_left(HEAVY); + hline_middle_right(LIGHT); + vline_middle_up(LIGHT); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_right_down_heavy_and_left_up_light(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right(HEAVY); + vline_middle_up(LIGHT); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_down_light_and_up_horizontal_heavy(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle_up(HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_up_light_and_down_horizontal_heavy(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle_up(LIGHT); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_right_light_and_left_vertical_heavy(struct buf *buf) +{ + hline_middle_left(HEAVY); + hline_middle_right(LIGHT); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_left_light_and_right_vertical_heavy(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right(HEAVY); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_heavy_vertical_and_horizontal(struct buf *buf) +{ + hline_middle(HEAVY); + vline_middle(HEAVY); +} + +static void +draw_box_drawings_light_double_dash_horizontal(struct buf *buf) +{ + draw_box_drawings_dash_horizontal(buf, 2, thickness(LIGHT), thickness(LIGHT)); +} + +static void +draw_box_drawings_heavy_double_dash_horizontal(struct buf *buf) +{ + draw_box_drawings_dash_horizontal(buf, 2, thickness(HEAVY), thickness(LIGHT)); +} + +static void +draw_box_drawings_light_double_dash_vertical(struct buf *buf) +{ + draw_box_drawings_dash_vertical(buf, 2, thickness(LIGHT), thickness(HEAVY)); +} + +static void +draw_box_drawings_heavy_double_dash_vertical(struct buf *buf) +{ + draw_box_drawings_dash_vertical(buf, 2, thickness(HEAVY), thickness(HEAVY)); +} + +static void +draw_box_drawings_double_horizontal(struct buf *buf) +{ + int thick = thickness(LIGHT); + int mid = (buf->height - thick * 3) / 2; + + hline(0, buf->width, mid, thick); + hline(0, buf->width, mid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_vertical(struct buf *buf) +{ + int thick = thickness(LIGHT); + int mid = (buf->width - thick * 3) / 2; + + vline(0, buf->height, mid, thick); + vline(0, buf->height, mid + 2 * thick, thick); +} + +static void +draw_box_drawings_down_single_and_right_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick) / 2; + + vline_middle_down(LIGHT); + + hline(vmid, buf->width, hmid, thick); + hline(vmid, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_down_double_and_right_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline_middle_right(LIGHT); + + vline(hmid, buf->height, vmid, thick); + vline(hmid, buf->height, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_down_and_right(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + vline(hmid, buf->height, vmid, thick); + vline(hmid + 2 * thick, buf->height, vmid + 2 * thick, thick); + + hline(vmid, buf->width, hmid, thick); + hline(vmid + 2 * thick, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_down_single_and_left_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width + thick) / 2; + + vline_middle_down(LIGHT); + + hline(0, vmid, hmid, thick); + hline(0, vmid, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_down_double_and_left_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline_middle_left(LIGHT); + + vline(hmid, buf->height, vmid, thick); + vline(hmid, buf->height, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_down_and_left(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + vline(hmid + 2 * thick, buf->height, vmid, thick); + vline(hmid, buf->height, vmid + 2 * thick, thick); + + hline(0, vmid + 2 * thick, hmid, thick); + hline(0, vmid, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_up_single_and_right_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick) / 2; + + vline_middle_up(LIGHT); + + hline(vmid, buf->width, hmid, thick); + hline(vmid, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_up_double_and_right_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height + thick) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline_middle_right(LIGHT); + + vline(0, hmid, vmid, thick); + vline(0, hmid, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_up_and_right(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + vline(0, hmid + 2 * thick, vmid, thick); + vline(0, hmid, vmid + 2 * thick, thick); + + hline(vmid + 2 * thick, buf->width, hmid, thick); + hline(vmid, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_up_single_and_left_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width + thick) / 2; + + vline_middle_up(LIGHT); + + hline(0, vmid, hmid, thick); + hline(0, vmid, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_up_double_and_left_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height + thick) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline_middle_left(LIGHT); + + vline(0, hmid, vmid, thick); + vline(0, hmid, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_up_and_left(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + vline(0, hmid + 0 * thick + thick, vmid, thick); + vline(0, hmid + 2 * thick + thick, vmid + 2 * thick, thick); + + hline(0, vmid, hmid, thick); + hline(0, vmid + 2 * thick, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_vertical_single_and_right_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick) / 2; + + vline_middle(LIGHT); + + hline(vmid, buf->width, hmid, thick); + hline(vmid, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_vertical_double_and_right_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int vmid = (buf->width - thick * 3) / 2; + + hline(vmid + 2 * thick, buf->width, (buf->height - thick) / 2, thick); + + vline(0, buf->height, vmid, thick); + vline(0, buf->height, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_vertical_and_right(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + vline(0, buf->height, vmid, thick); + vline(0, hmid, vmid + 2 * thick, thick); + vline(hmid + 2 * thick, buf->height, vmid + 2 * thick, thick); + + hline(vmid + 2 * thick, buf->width, hmid, thick); + hline(vmid + 2 * thick, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_vertical_single_and_left_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width + thick) / 2; + + vline_middle(LIGHT); + + hline(0, vmid, hmid, thick); + hline(0, vmid, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_vertical_double_and_left_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int vmid = (buf->width - thick * 3) / 2; + + hline(0, vmid, (buf->height - thick) / 2, thick); + + vline(0, buf->height, vmid, thick); + vline(0, buf->height, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_vertical_and_left(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + vline(0, buf->height, vmid + 2 * thick, thick); + vline(0, hmid, vmid, thick); + vline(hmid + 2 * thick, buf->height, vmid, thick); + + hline(0, vmid + thick, hmid, thick); + hline(0, vmid, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_down_single_and_horizontal_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + + vline(hmid + 2 * thick, buf->height, (buf->width - thick) / 2, thick); + + hline(0, buf->width, hmid, thick); + hline(0, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_down_double_and_horizontal_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline_middle(LIGHT); + + vline(hmid, buf->height, vmid, thick); + vline(hmid, buf->height, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_down_and_horizontal(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline(0, buf->width, hmid, thick); + hline(0, vmid, hmid + 2 * thick, thick); + hline(vmid + 2 * thick, buf->width, hmid + 2 * thick, thick); + + vline(hmid + 2 * thick, buf->height, vmid, thick); + vline(hmid + 2 * thick, buf->height, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_up_single_and_horizontal_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick) / 2; + + vline(0, hmid, vmid, thick); + + hline(0, buf->width, hmid, thick); + hline(0, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_up_double_and_horizontal_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline_middle(LIGHT); + + vline(0, hmid, vmid, thick); + vline(0, hmid, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_up_and_horizontal(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + vline(0, hmid, vmid, thick); + vline(0, hmid, vmid + 2 * thick, thick); + + hline(0, vmid + thick, hmid, thick); + hline(vmid + 2 * thick, buf->width, hmid, thick); + hline(0, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_vertical_single_and_horizontal_double(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + + vline_middle(LIGHT); + + hline(0, buf->width, hmid, thick); + hline(0, buf->width, hmid + 2 * thick, thick); +} + +static void +draw_box_drawings_vertical_double_and_horizontal_single(struct buf *buf) +{ + int thick = thickness(LIGHT); + int vmid = (buf->width - thick * 3) / 2; + + hline_middle(LIGHT); + + vline(0, buf->height, vmid, thick); + vline(0, buf->height, vmid + 2 * thick, thick); +} + +static void +draw_box_drawings_double_vertical_and_horizontal(struct buf *buf) +{ + int thick = thickness(LIGHT); + int hmid = (buf->height - thick * 3) / 2; + int vmid = (buf->width - thick * 3) / 2; + + hline(0, vmid, hmid, thick); + hline(vmid + 2 * thick, buf->width, hmid, thick); + hline(0, vmid, hmid + 2 * thick, thick); + hline(vmid + 2 * thick, buf->width, hmid + 2 * thick, thick); + + vline(0, hmid + thick, vmid, thick); + vline(0, hmid, vmid + 2 * thick, thick); + vline(hmid + 2 * thick, buf->height, vmid, thick); + vline(hmid + 2 * thick, buf->height, vmid + 2 * thick, thick); +} + +static inline void +set_a1_bit(uint8_t *data, size_t ofs, size_t bit_no) +{ +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + data[ofs] |= 1 << bit_no; +#else + data[ofs] |= 1 << (7 - bit_no); +#endif +} + +static void NOINLINE +draw_box_drawings_light_arc(struct buf *buf, char32_t wc) +{ + const pixman_format_code_t fmt = buf->format; + const int supersample = fmt == PIXMAN_a8 ? 4 : 1; + const int height = buf->height * supersample; + const int width = buf->width * supersample; + const int stride = fmt == PIXMAN_a8 + ? stride_for_format_and_width(PIXMAN_a8, width) : buf->stride; + uint8_t *data = supersample > 1 ? xcalloc(height * stride, 1) : buf->data; + + const int thick = thickness(LIGHT) * supersample; + + const int height_pixels = buf->height; + const int width_pixels = buf->width; + const int thick_pixels = thickness(LIGHT); + + /* + * The general idea here is to connect the two incoming lines using a + * circle, which is extended to the box-edges with vertical/horizontal + * lines. + * + * The radius of the quartercircle should be as big as possible, with some + * restrictions: The radius should be the same for all of ╭ ╮ ╯ ╰ at a + * given box-size (which won't be the case if we choose the biggest + * possible radius for a given box, consider the following:) + * + * + * ▕ ███ ▏ + * ▕a │ d▔▔ + * ▕ │ x + * ▕ │ + * ▕ │ ██ + * ▕ ╰────██ + * ▕ ██ + * ▕ + * ▕ + * ▕b c + * ▔▔▔▔▔▔▔▔▔▔ + * for ╰ it would be possible to center the circle on the upper right + * corner of d, but we have set it on x instead because ╯ can only use a + * 2px inner radius: + * + * ▕ ███ ▏ + * ▔▔a │ d▏ + * x │ ▏ + * │ ▏ + * ██ │ ▏ + * ██───╯ ▏ + * ██ ▏ + * ▏ + * ▏ + * b c▏ + * ▔▔▔▔▔▔▔▔▔▔ + * As the incoming lines always exactly fill pixels, and are rounded down + * (via float->int), we can use this to get the radius of the inner + * (connecting the left/upper edge of the lines) quartercircle. + */ + int circle_inner_edge = (min(width_pixels, height_pixels) - thick_pixels) / 2; + + /* + * We want to draw the quartercircle by filling small circles (with r = + * thickness/2.) whose centers are on its edge. This means to get the + * radius of the quartercircle, we add the exact half thickness to the + * radius of the inner circle. + */ + double c_r = circle_inner_edge + thick_pixels/2.; + + /* + * We need to draw short lines from the end of the quartercircle to the + * box-edges, store one endpoint (the other is the edge of the + * quartercircle) in these vars. + */ + int vert_to = 0, + hor_to = 0; + + /* Coordinates of the circle-center. */ + int c_x = 0, + c_y = 0; + + /* + * For a given y there are up to two solutions for the circle-equation. + * Set to -1 for the left, and 1 for the right hemisphere. + */ + int circle_hemisphere = 0; + + /* + * The quarter circle only has to be evaluated for a small range of + * y-values. + */ + int y_min = 0, + y_max = 0; + + switch (wc) { + case U'╭': { + /* + * Don't use supersampled coordinates yet, we want to align actual + * pixels. + * + * pixel-coordinates of the lower edge of the right line and the + * right edge of the bottom line. + */ + int right_bottom_edge = (height_pixels + thick_pixels) / 2; + int bottom_right_edge = (width_pixels + thick_pixels) / 2; + + /* find coordinates of circle-center. */ + c_y = right_bottom_edge + circle_inner_edge; + c_x = bottom_right_edge + circle_inner_edge; + + /* we want to render the left, not the right hemisphere of the circle. */ + circle_hemisphere = -1; + + /* don't evaluate beyond c_y, the vertical line is drawn there. */ + y_min = 0; + y_max = c_y; + + /* + * the vertical line should extend to the bottom of the box, the + * horizontal to the right. + */ + vert_to = height_pixels; + hor_to = width_pixels; + + break; + } + case U'╮': { + int left_bottom_edge = (height_pixels + thick_pixels) / 2; + int bottom_left_edge = (width_pixels - thick_pixels) / 2; + + c_y = left_bottom_edge + circle_inner_edge; + c_x = bottom_left_edge - circle_inner_edge; + + circle_hemisphere = 1; + + y_min = 0; + y_max = c_y; + + vert_to = height_pixels; + hor_to = 0; + + break; + } + case U'╰': { + int right_top_edge = (height_pixels - thick_pixels) / 2; + int top_right_edge = (width_pixels + thick_pixels) / 2; + + c_y = right_top_edge - circle_inner_edge; + c_x = top_right_edge + circle_inner_edge; + + circle_hemisphere = -1; + + y_min = c_y; + y_max = height_pixels; + + vert_to = 0; + hor_to = width_pixels; + + break; + } + case U'╯': { + int left_top_edge = (height_pixels - thick_pixels) / 2; + int top_left_edge = (width_pixels - thick_pixels) / 2; + + c_y = left_top_edge - circle_inner_edge; + c_x = top_left_edge - circle_inner_edge; + + circle_hemisphere = 1; + + y_min = c_y; + y_max = height_pixels; + + vert_to = 0; + hor_to = 0; + + break; + } + } + + /* store for horizontal+vertical line. */ + int c_x_pixels = c_x; + int c_y_pixels = c_y; + + /* Bring coordinates from pixel-grid to supersampled grid. */ + c_r *= supersample; + c_x *= supersample; + c_y *= supersample; + + y_min *= supersample; + y_max *= supersample; + + double c_r2 = c_r * c_r; + + /* + * To prevent gaps in the circle, each pixel is sampled multiple times. + * As the quartercircle ends (vertically) in the middle of a pixel, an + * uneven number helps hit that exactly. + */ + for (double i = y_min*16; i <= y_max*16; i++) { + errno = 0; + + double y = i / 16.; + double x = circle_hemisphere * sqrt(c_r2 - (y - c_y) * (y - c_y)) + c_x; + + /* See math_error(7) */ + if (errno != 0) + { + continue; + } + + const int row = round(y); + const int col = round(x); + + if (col < 0) + continue; + + /* rectangle big enough to fit entire circle with radius thick/2. */ + int row1 = row - (thick/2+1); + int row2 = row + (thick/2+1); + int col1 = col - (thick/2+1); + int col2 = col + (thick/2+1); + + int row_start = min(row1, row2), + row_end = max(row1, row2), + col_start = min(col1, col2), + col_end = max(col1, col2); + + xassert(row_end > row_start); + xassert(col_end > col_start); + + /* + * draw circle with radius thick/2 around x,y. + * this is accomplished by rejecting pixels where the distance from + * their center to x,y is greater than thick/2. + */ + for (int r = max(row_start, 0); r < max(min(row_end, height), 0); r++) { + double r_midpoint = r + 0.5; + for (int c = max(col_start, 0); c < max(min(col_end, width), 0); c++) { + double c_midpoint = c + 0.5; + + /* vector from point on quartercircle to midpoint of the current pixel. */ + double center_midpoint_x = c_midpoint - x; + double center_midpoint_y = r_midpoint - y; + + /* distance from current point to circle-center. */ + double dist = sqrt(center_midpoint_x * center_midpoint_x + center_midpoint_y * center_midpoint_y); + /* skip if midpoint of pixel is outside the circle. */ + if (dist > thick/2.) + continue; + if (fmt == PIXMAN_a1) { + size_t idx = c / 8; + size_t bit_no = c % 8; + set_a1_bit(data, r * stride + idx, bit_no); + } else + data[r * stride + c] = 0xff; + } + } + } + + if (fmt == PIXMAN_a8) { + xassert(data != buf->data); + + /* Downsample */ + for (size_t r = 0; r < buf->height; r++) { + for (size_t c = 0; c < buf->width; c++) { + uint32_t total = 0; + for (size_t i = 0; i < supersample; i++) { + for (size_t j = 0; j < supersample; j++) + total += data[(r * supersample + i) * stride + c * supersample + j]; + } + uint8_t average = min(total / (supersample * supersample), 0xff); + buf->data[r * buf->stride + c] = average; + } + } + + free(data); + } + + /* draw vertical/horizontal lines from quartercircle-edge to box-edge. */ + vline(min(c_y_pixels, vert_to), max(c_y_pixels, vert_to), (width_pixels - thick_pixels) / 2, thick_pixels); + hline(min(c_x_pixels, hor_to), max(c_x_pixels, hor_to), (height_pixels - thick_pixels) / 2, thick_pixels); +} + +static void NOINLINE +draw_box_drawings_light_diagonal_upper_right_to_lower_left(struct buf *buf) +{ + pixman_trapezoid_t trap = { + .top = pixman_int_to_fixed(0), + .bottom = pixman_int_to_fixed(buf->height), + .left = { + .p1 = { + .x = pixman_double_to_fixed(buf->width - thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(0), + }, + .p2 = { + .x = pixman_double_to_fixed(0 - thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(buf->height), + }, + }, + .right = { + .p1 = { + .x = pixman_double_to_fixed(buf->width + thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(0), + }, + .p2 = { + .x = pixman_double_to_fixed(0 + thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(buf->height), + }, + }, + }; + + pixman_rasterize_trapezoid(buf->pix, &trap, 0, 0); +} + +static void NOINLINE +draw_box_drawings_light_diagonal_upper_left_to_lower_right(struct buf *buf) +{ + pixman_trapezoid_t trap = { + .top = pixman_int_to_fixed(0), + .bottom = pixman_int_to_fixed(buf->height), + .left = { + .p1 = { + .x = pixman_double_to_fixed(0 - thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(0), + }, + .p2 = { + .x = pixman_double_to_fixed(buf->width - thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(buf->height), + }, + }, + .right = { + .p1 = { + .x = pixman_double_to_fixed(0 + thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(0), + }, + .p2 = { + .x = pixman_double_to_fixed(buf->width + thickness(LIGHT) / 2.), + .y = pixman_int_to_fixed(buf->height), + }, + }, + }; + + pixman_rasterize_trapezoid(buf->pix, &trap, 0, 0); +} + +static void +draw_box_drawings_light_diagonal_cross(struct buf *buf) +{ + draw_box_drawings_light_diagonal_upper_right_to_lower_left(buf); + draw_box_drawings_light_diagonal_upper_left_to_lower_right(buf); +} + +static void +draw_box_drawings_light_left(struct buf *buf) +{ + hline_middle_left(LIGHT); +} + +static void +draw_box_drawings_light_up(struct buf *buf) +{ + vline_middle_up(LIGHT); +} + +static void +draw_box_drawings_light_right(struct buf *buf) +{ + hline_middle_right(LIGHT); +} + +static void +draw_box_drawings_light_down(struct buf *buf) +{ + vline_middle_down(LIGHT); +} + +static void +draw_box_drawings_heavy_left(struct buf *buf) +{ + hline_middle_left(HEAVY); +} + +static void +draw_box_drawings_heavy_up(struct buf *buf) +{ + vline_middle_up(HEAVY); +} + +static void +draw_box_drawings_heavy_right(struct buf *buf) +{ + hline_middle_right(HEAVY); +} + +static void +draw_box_drawings_heavy_down(struct buf *buf) +{ + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_light_left_and_heavy_right(struct buf *buf) +{ + hline_middle_left(LIGHT); + hline_middle_right(HEAVY); +} + +static void +draw_box_drawings_light_up_and_heavy_down(struct buf *buf) +{ + vline_middle_up(LIGHT); + vline_middle_down(HEAVY); +} + +static void +draw_box_drawings_heavy_left_and_light_right(struct buf *buf) +{ + hline_middle_left(HEAVY); + hline_middle_right(LIGHT); +} + +static void +draw_box_drawings_heavy_up_and_light_down(struct buf *buf) +{ + vline_middle_up(HEAVY); + vline_middle_down(LIGHT); +} + +static void +draw_upper_half_block(struct buf *buf) +{ + rect(0, 0, buf->width, round(buf->height / 2.)); +} + +static void +draw_lower_one_eighth_block(struct buf *buf) +{ + rect(0, buf->height - round(buf->height / 8.), buf->width, buf->height); +} + +static void +draw_lower_one_quarter_block(struct buf *buf) +{ + rect(0, buf->height - round(buf->height / 4.), buf->width, buf->height); +} + +static void +draw_lower_three_eighths_block(struct buf *buf) +{ + rect(0, buf->height - round(3. * buf->height / 8.), buf->width, buf->height); +} + +static void +draw_lower_half_block(struct buf *buf) +{ + rect(0, buf->height - round(buf->height / 2.), buf->width, buf->height); +} + +static void +draw_lower_five_eighths_block(struct buf *buf) +{ + rect(0, buf->height - round(5. * buf->height / 8.), buf->width, buf->height); +} + +static void +draw_lower_three_quarters_block(struct buf *buf) +{ + rect(0, buf->height - round(3. * buf->height / 4.), buf->width, buf->height); +} + +static void +draw_lower_seven_eighths_block(struct buf *buf) +{ + rect(0, buf->height - round(7. * buf->height / 8.), buf->width, buf->height); +} + +static void +draw_upper_one_quarter_block(struct buf *buf) +{ + rect(0, 0, buf->width, round(buf->height / 4.)); +} + +static void +draw_upper_three_eighths_block(struct buf *buf) +{ + rect(0, 0, buf->width, round(3. * buf->height / 8.)); +} + +static void +draw_upper_five_eighths_block(struct buf *buf) +{ + rect(0, 0, buf->width, round(5. * buf->height / 8.)); +} + +static void +draw_upper_three_quarters_block(struct buf *buf) +{ + rect(0, 0, buf->width, round(3. * buf->height / 4.)); +} + +static void +draw_upper_seven_eighths_block(struct buf *buf) +{ + rect(0, 0, buf->width, round(7. * buf->height / 8.)); +} + +static void +draw_full_block(struct buf *buf) +{ + rect(0, 0, buf->width, buf->height); +} + +static void +draw_left_seven_eighths_block(struct buf *buf) +{ + rect(0, 0, round(7. * buf->width / 8.), buf->height); +} + +static void +draw_left_three_quarters_block(struct buf *buf) +{ + rect(0, 0, round(3. * buf->width / 4.), buf->height); +} + +static void +draw_left_five_eighths_block(struct buf *buf) +{ + rect(0, 0, round(5. * buf->width / 8.), buf->height); +} + +static void +draw_left_half_block(struct buf *buf) +{ + rect(0, 0, round(buf->width / 2.), buf->height); +} + +static void +draw_left_three_eighths_block(struct buf *buf) +{ + rect(0, 0, round(3. * buf->width / 8.), buf->height); +} + +static void +draw_left_one_quarter_block(struct buf *buf) +{ + rect(0, 0, round(buf->width / 4.), buf->height); +} + +static void +draw_vertical_one_eighth_block_n(struct buf *buf, int n) +{ + double x = round((double)n * buf->width / 8.); + double w = round(buf->width / 8.); + rect(x, 0, x + w, buf->height); +} + +static void +draw_left_one_eighth_block(struct buf *buf) +{ + draw_vertical_one_eighth_block_n(buf, 0); +} + +static void +draw_vertical_one_eighth_block_2(struct buf *buf) +{ + draw_vertical_one_eighth_block_n(buf, 1); +} + +static void +draw_vertical_one_eighth_block_3(struct buf *buf) +{ + draw_vertical_one_eighth_block_n(buf, 2); +} + +static void +draw_vertical_one_eighth_block_4(struct buf *buf) +{ + draw_vertical_one_eighth_block_n(buf, 3); +} + +static void +draw_vertical_one_eighth_block_5(struct buf *buf) +{ + draw_vertical_one_eighth_block_n(buf, 4); +} + +static void +draw_vertical_one_eighth_block_6(struct buf *buf) +{ + draw_vertical_one_eighth_block_n(buf, 5); +} + +static void +draw_vertical_one_eighth_block_7(struct buf *buf) +{ + draw_vertical_one_eighth_block_n(buf, 6); +} + +static void +draw_right_half_block(struct buf *buf) +{ + rect(round(buf->width / 2.), 0, buf->width, buf->height); +} + +static void NOINLINE +draw_pixman_shade(struct buf *buf, uint16_t v) +{ + pixman_color_t shade = {.red = 0, .green = 0, .blue = 0, .alpha = v}; + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix, &shade, 1, + (pixman_rectangle16_t []){{0, 0, buf->width, buf->height}}); +} + +static void +draw_light_shade(struct buf *buf) +{ + pixman_format_code_t fmt = buf->format; + + if (buf->solid_shades && fmt == PIXMAN_a1) + change_buffer_format(buf, PIXMAN_a8); + else if (!buf->solid_shades && fmt == PIXMAN_a8) + change_buffer_format(buf, PIXMAN_a1); + + if (buf->solid_shades) + draw_pixman_shade(buf, 0x4000); + else { + for (size_t row = 0; row < buf->height; row += 2) { + for (size_t col = 0; col < buf->width; col += 2) { + size_t idx = col / 8; + size_t bit_no = col % 8; + set_a1_bit(buf->data, row * buf->stride + idx, bit_no); + } + } + } +} + +static void +draw_medium_shade(struct buf *buf) +{ + pixman_format_code_t fmt = buf->format; + + if (buf->solid_shades && fmt == PIXMAN_a1) + change_buffer_format(buf, PIXMAN_a8); + else if (!buf->solid_shades && fmt == PIXMAN_a8) + change_buffer_format(buf, PIXMAN_a1); + + if (buf->solid_shades) + draw_pixman_shade(buf, 0x8000); + else { + for (size_t row = 0; row < buf->height; row++) { + for (size_t col = row % 2; col < buf->width; col += 2) { + size_t idx = col / 8; + size_t bit_no = col % 8; + set_a1_bit(buf->data, row * buf->stride + idx, bit_no); + } + } + } +} + +static void +draw_dark_shade(struct buf *buf) +{ + pixman_format_code_t fmt = buf->format; + + if (buf->solid_shades && fmt == PIXMAN_a1) + change_buffer_format(buf, PIXMAN_a8); + else if (!buf->solid_shades && fmt == PIXMAN_a8) + change_buffer_format(buf, PIXMAN_a1); + + if (buf->solid_shades) + draw_pixman_shade(buf, 0xc000); + else { + for (size_t row = 0; row < buf->height; row++) { + for (size_t col = 0; col < buf->width; col += 1 + row % 2) { + size_t idx = col / 8; + size_t bit_no = col % 8; + set_a1_bit(buf->data, row * buf->stride + idx, bit_no); + } + } + } +} + +static void NOINLINE +draw_horizontal_one_eighth_block_n(struct buf *buf, int n) +{ + double y = round((double)n * buf->height / 8.); + double h = round(buf->height / 8.); + rect(0, y, buf->width, y + h); +} + +static void +draw_upper_one_eighth_block(struct buf *buf) +{ + draw_horizontal_one_eighth_block_n(buf, 0); +} + +static void +draw_horizontal_one_eighth_block_2(struct buf *buf) +{ + draw_horizontal_one_eighth_block_n(buf, 1); +} + +static void +draw_horizontal_one_eighth_block_3(struct buf *buf) +{ + draw_horizontal_one_eighth_block_n(buf, 2); +} + +static void +draw_horizontal_one_eighth_block_4(struct buf *buf) +{ + draw_horizontal_one_eighth_block_n(buf, 3); +} + +static void +draw_horizontal_one_eighth_block_5(struct buf *buf) +{ + draw_horizontal_one_eighth_block_n(buf, 4); +} + +static void +draw_horizontal_one_eighth_block_6(struct buf *buf) +{ + draw_horizontal_one_eighth_block_n(buf, 5); +} + +static void +draw_horizontal_one_eighth_block_7(struct buf *buf) +{ + draw_horizontal_one_eighth_block_n(buf, 6); +} + +static void +draw_right_one_eighth_block(struct buf *buf) +{ + rect(buf->width - round(buf->width / 8.), 0, buf->width, buf->height); +} + +static void +quad_upper_left(struct buf *buf) +{ + rect(0, 0, ceil(buf->width / 2.), ceil(buf->height / 2.)); +} + +static void +quad_upper_right(struct buf *buf) +{ + rect(floor(buf->width / 2.), 0, buf->width, ceil(buf->height / 2.)); +} + +static void +quad_lower_left(struct buf *buf) +{ + rect(0, floor(buf->height / 2.), ceil(buf->width / 2.), buf->height); +} + +static void +quad_lower_right(struct buf *buf) +{ + rect(floor(buf->width / 2.), floor(buf->height / 2.), buf->width, buf->height); +} + +static void NOINLINE +draw_quadrant(struct buf *buf, char32_t wc) +{ + enum { + UPPER_LEFT = 1 << 0, + UPPER_RIGHT = 1 << 1, + LOWER_LEFT = 1 << 2, + LOWER_RIGHT = 1 << 3, + }; + + static const uint8_t matrix[10] = { + LOWER_LEFT, + LOWER_RIGHT, + UPPER_LEFT, + UPPER_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | LOWER_RIGHT, + UPPER_RIGHT, + UPPER_RIGHT | LOWER_LEFT, + UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, + }; + + xassert(wc >= 0x2596 && wc <= 0x259f); + const size_t idx = wc - 0x2596; + + xassert(idx < ALEN(matrix)); + uint8_t encoded = matrix[idx]; + + if (encoded & UPPER_LEFT) + quad_upper_left(buf); + + if (encoded & UPPER_RIGHT) + quad_upper_right(buf); + + if (encoded & LOWER_LEFT) + quad_lower_left(buf); + + if (encoded & LOWER_RIGHT) + quad_lower_right(buf); +} + +static void NOINLINE +draw_braille(struct buf *buf, char32_t wc) +{ + int w = min(buf->width / 4, buf->height / 8); + int x_spacing = buf->width / 4; + int y_spacing = buf->height / 8; + int x_margin = x_spacing / 2; + int y_margin = y_spacing / 2; + + int x_px_left = buf->width - 2 * x_margin - x_spacing - 2 * w; + int y_px_left = buf->height - 2 * y_margin - 3 * y_spacing - 4 * w; + + LOG_DBG( + "braille: before adjusting: " + "cell: %dx%d, margin=%dx%d, spacing=%dx%d, width=%d, left=%dx%d", + buf->width, buf->height, x_margin, y_margin, x_spacing, y_spacing, + w, x_px_left, y_px_left); + + /* First, try hard to ensure the DOT width is non-zero */ + if (x_px_left >= 2 && y_px_left >= 4 && w == 0) { + w++; + x_px_left -= 2; + y_px_left -= 4; + } + + /* Second, prefer a non-zero margin */ + if (x_px_left >= 2 && x_margin == 0) { x_margin = 1; x_px_left -= 2; } + if (y_px_left >= 2 && y_margin == 0) { y_margin = 1; y_px_left -= 2; } + + /* Third, increase spacing */ + if (x_px_left >= 1) { x_spacing++; x_px_left--; } + if (y_px_left >= 3) { y_spacing++; y_px_left -= 3; } + + /* Fourth, margins ("spacing", but on the sides) */ + if (x_px_left >= 2) { x_margin++; x_px_left -= 2; } + if (y_px_left >= 2) { y_margin++; y_px_left -= 2; } + + /* Last - increase dot width */ + if (x_px_left >= 2 && y_px_left >= 4) { + w++; + x_px_left -= 2; + y_px_left -= 4; + } + + LOG_DBG( + "braille: after adjusting: " + "cell: %dx%d, margin=%dx%d, spacing=%dx%d, width=%d, left=%dx%d", + buf->width, buf->height, x_margin, y_margin, x_spacing, y_spacing, + w, x_px_left, y_px_left); + + xassert(x_px_left <= 1 || y_px_left <= 1); + xassert(2 * x_margin + 2 * w + x_spacing <= buf->width); + xassert(2 * y_margin + 4 * w + 3 * y_spacing <= buf->height); + + int x[2], y[4]; + x[0] = x_margin; + x[1] = x_margin + w + x_spacing; + y[0] = y_margin; + y[1] = y[0] + w + y_spacing; + y[2] = y[1] + w + y_spacing; + y[3] = y[2] + w + y_spacing; + + assert(wc >= 0x2800); + assert(wc <= 0x28ff); + uint8_t sym = wc - 0x2800; + + /* Left side */ + if (sym & 1) + rect(x[0], y[0], x[0] + w, y[0] + w); + if (sym & 2) + rect(x[0], y[1], x[0] + w, y[1] + w); + if (sym & 4) + rect(x[0], y[2], x[0] + w, y[2] + w); + + /* Right side */ + if (sym & 8) + rect(x[1], y[0], x[1] + w, y[0] + w); + if (sym & 16) + rect(x[1], y[1], x[1] + w, y[1] + w); + if (sym & 32) + rect(x[1], y[2], x[1] + w, y[2] + w); + + /* 8-dot patterns */ + if (sym & 64) + rect(x[0], y[3], x[0] + w, y[3] + w); + if (sym & 128) + rect(x[1], y[3], x[1] + w, y[3] + w); +} + +static void +sextant_upper_left(struct buf *buf) +{ + rect(0, 0, buf->x_halfs[0], buf->y_thirds[0]); +} + +static void +sextant_middle_left(struct buf *buf) +{ + rect(0, buf->y_thirds[0], buf->x_halfs[0], buf->y_thirds[1]); +} + +static void +sextant_lower_left(struct buf *buf) +{ + rect(0, buf->y_thirds[1], buf->x_halfs[0], buf->height); +} + +static void +sextant_upper_right(struct buf *buf) +{ + rect(buf->x_halfs[1], 0, buf->width, buf->y_thirds[0]); +} + +static void +sextant_middle_right(struct buf *buf) +{ + rect(buf->x_halfs[1], buf->y_thirds[0], buf->width, buf->y_thirds[1]); +} + +static void +sextant_lower_right(struct buf *buf) +{ + rect(buf->x_halfs[1], buf->y_thirds[1], buf->width, buf->height); +} + +static void NOINLINE +draw_sextant(struct buf *buf, char32_t wc) +{ + /* + * Each byte encodes one sextant: + * + * Bit sextant + * 0 upper left + * 1 middle left + * 2 lower left + * 3 upper right + * 4 middle right + * 5 lower right + */ + enum { + UPPER_LEFT = 1 << 0, + MIDDLE_LEFT = 1 << 1, + LOWER_LEFT = 1 << 2, + UPPER_RIGHT = 1 << 3, + MIDDLE_RIGHT = 1 << 4, + LOWER_RIGHT = 1 << 5, + }; + + /* TODO: move this to a separate file? */ + static const uint8_t matrix[60] = { + /* U+1fb00 - U+1fb0f */ + UPPER_LEFT, + UPPER_RIGHT, + UPPER_LEFT | UPPER_RIGHT, + MIDDLE_LEFT, + UPPER_LEFT | MIDDLE_LEFT, + UPPER_RIGHT | MIDDLE_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT, + MIDDLE_RIGHT, + UPPER_LEFT | MIDDLE_RIGHT, + UPPER_RIGHT | MIDDLE_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_RIGHT, + MIDDLE_LEFT | MIDDLE_RIGHT, + UPPER_LEFT | MIDDLE_LEFT | MIDDLE_RIGHT, + UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT, + LOWER_LEFT, + + /* U+1fb10 - U+1fb1f */ + UPPER_LEFT | LOWER_LEFT, + UPPER_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT, + MIDDLE_LEFT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_LEFT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | LOWER_LEFT, + MIDDLE_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_RIGHT | LOWER_LEFT, + MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT, + LOWER_RIGHT, + UPPER_LEFT | LOWER_RIGHT, + + /* U+1fb20 - U+1fb2f */ + UPPER_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | LOWER_RIGHT, + MIDDLE_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | LOWER_RIGHT, + MIDDLE_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_RIGHT | LOWER_RIGHT, + MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_RIGHT, + LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, + + /* U+1fb30 - U+1fb3b */ + UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_LEFT | MIDDLE_RIGHT | LOWER_LEFT | LOWER_RIGHT, + }; + + xassert(wc >= 0x1fb00 && wc <= 0x1fb3b); + const size_t idx = wc - 0x1fb00; + + xassert(idx < ALEN(matrix)); + uint8_t encoded = matrix[idx]; + + if (encoded & UPPER_LEFT) + sextant_upper_left(buf); + + if (encoded & MIDDLE_LEFT) + sextant_middle_left(buf); + + if (encoded & LOWER_LEFT) + sextant_lower_left(buf); + + if (encoded & UPPER_RIGHT) + sextant_upper_right(buf); + + if (encoded & MIDDLE_RIGHT) + sextant_middle_right(buf); + + if (encoded & LOWER_RIGHT) + sextant_lower_right(buf); +} + +static void +octant_upper_left(struct buf *buf) +{ + rect(0, 0, buf->x_halfs[0], buf->y_quads[0]); +} + +static void +octant_middle_up_left(struct buf *buf) +{ + rect(0, buf->y_quads[0], buf->x_halfs[0], buf->y_quads[1]); +} + +static void +octant_middle_down_left(struct buf *buf) +{ + rect(0, buf->y_quads[1], buf->x_halfs[0], buf->y_quads[2]); +} + +static void +octant_lower_left(struct buf *buf) +{ + rect(0, buf->y_quads[2], buf->x_halfs[0], buf->height); +} + +static void +octant_upper_right(struct buf *buf) +{ + rect(buf->x_halfs[1], 0, buf->width, buf->y_quads[0]); +} + +static void +octant_middle_up_right(struct buf *buf) +{ + rect(buf->x_halfs[1], buf->y_quads[0], buf->width, buf->y_quads[1]); +} + +static void +octant_middle_down_right(struct buf *buf) +{ + rect(buf->x_halfs[1], buf->y_quads[1], buf->width, buf->y_quads[2]); +} + +static void +octant_lower_right(struct buf *buf) +{ + rect(buf->x_halfs[1], buf->y_quads[2], buf->width, buf->height); +} + +static void NOINLINE +draw_octant(struct buf *buf, char32_t wc) +{ + /* + * Each byte encodes one octant: + * + * Bit octant part + * 0 upper left + * 1 middle, upper left + * 2 middle, lower left + * 3 lower, left + * 4 upper right + * 5 middle, upper right + * 6 middle, lower right + * 7 lower right + */ + enum { + UPPER_LEFT = 1 << 0, + MIDDLE_UP_LEFT = 1 << 1, + MIDDLE_DOWN_LEFT = 1 << 2, + LOWER_LEFT = 1 << 3, + UPPER_RIGHT = 1 << 4, + MIDDLE_UP_RIGHT = 1 << 5, + MIDDLE_DOWN_RIGHT = 1 << 6, + LOWER_RIGHT = 1 << 7, + }; + + /* TODO: move this to a separate file */ + static const uint8_t matrix[230] = { + /* U+1CD00 - U+1CD0F */ + MIDDLE_UP_LEFT, + MIDDLE_UP_LEFT | UPPER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | UPPER_RIGHT, + MIDDLE_UP_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT, + MIDDLE_DOWN_LEFT, + UPPER_LEFT | MIDDLE_DOWN_LEFT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT, + + /* U+1CD10 - U+1CD1F */ + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT, + MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT, + + /* U+1CD20 - U+1CD2F */ + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT, + MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + + /* U+1CD30 - U+1CD3F */ + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT, + UPPER_LEFT | LOWER_LEFT, + UPPER_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT, + MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT, + + /* U+1CD40 - U+1CD4F */ + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + + /* U+1CD50 - U+1CD5F */ + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT, + MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + + /* U+1CD60 - U+1CD6F */ + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + + /* U+1CD70 - U+1CD7F */ + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT, + UPPER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | LOWER_RIGHT, + MIDDLE_UP_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_RIGHT, + + /* U+1CD80 - U+1CD8F */ + MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_RIGHT, + + /* U+1CD90 - U+1CD9F */ + UPPER_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + + /* U+1CDA0 - U+1CDAF */ + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_RIGHT, + UPPER_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, + + /* U+1CDB0 - U+1CDBF */ + UPPER_LEFT | MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + + /* U+1CDC0 - U+1CDCF */ + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + + /* U+1CDD0 - U+1CDDF */ + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + + /* U+1CDE0 - U+1CDE5 */ + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | UPPER_RIGHT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_LEFT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + UPPER_RIGHT | MIDDLE_UP_LEFT | MIDDLE_UP_RIGHT | MIDDLE_DOWN_LEFT | MIDDLE_DOWN_RIGHT | LOWER_LEFT | LOWER_RIGHT, + }; + + _Static_assert(ALEN(matrix) == 230, "incorrect number of codepoints"); + +#if defined(_DEBUG) + const size_t last_implemented = 0x1cde5; + for (size_t i = 0; i < sizeof(matrix) / sizeof(matrix[0]); i++) { + if (i + 0x1cd00 > last_implemented) + break; + + for (size_t j = 0; j < sizeof(matrix) / sizeof(matrix[0]); j++) { + if (j + 0x1cd00 > last_implemented) + break; + + if (i == j) + continue; + + if (matrix[i] == matrix[j]) { + BUG("octant U+%05x (idx=%zu) is the same as U+%05x (idx=%zu)", + matrix[i], i, matrix[j], j); + } + } + } +#endif + + xassert(wc >= 0x1cd00 && wc <= 0x1cde5); + const size_t idx = wc - 0x1cd00; + + xassert(idx < ALEN(matrix)); + uint8_t encoded = matrix[idx]; + + if (encoded & UPPER_LEFT) + octant_upper_left(buf); + + if (encoded & MIDDLE_UP_LEFT) + octant_middle_up_left(buf); + + if (encoded & MIDDLE_DOWN_LEFT) + octant_middle_down_left(buf); + + if (encoded & LOWER_LEFT) + octant_lower_left(buf); + + if (encoded & UPPER_RIGHT) + octant_upper_right(buf); + + if (encoded & MIDDLE_UP_RIGHT) + octant_middle_up_right(buf); + + if (encoded & MIDDLE_DOWN_RIGHT) + octant_middle_down_right(buf); + + if (encoded & LOWER_RIGHT) + octant_lower_right(buf); +} + +static void NOINLINE +draw_wedge_triangle(struct buf *buf, char32_t wc) +{ + const int width = buf->width; + const int height = buf->height; + + int halfs0 = buf->x_halfs[0]; + int halfs1 = buf->x_halfs[1]; + int thirds0 = buf->y_thirds[0]; + int thirds1 = buf->y_thirds[1]; + + int p1_x, p1_y, p2_x, p2_y, p3_x, p3_y; + + switch (wc) { + case 0x1fb3c: /* 🬼 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb52: /* 🭒 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb3d: /* 🬽 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb53: /* 🭓 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb3e: /* 🬾 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb54: /* 🭔 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb3f: /* 🬿 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb55: /* 🭕 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb40: /* 🭀 */ + case 0x1fb56: /* 🭖 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = 0; p2_y = p3_y = height; + break; + + case 0x1fb47: /* 🭇 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb5d: /* 🭝 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb48: /* 🭈 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb5e: /* 🭞 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = thirds1; p2_y = p3_y = height; + break; + + case 0x1fb49: /* 🭉 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb5f: /* 🭟 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb4a: /* 🭊 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb60: /* 🭠 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = thirds0; p2_y = p3_y = height; + break; + + case 0x1fb4b: /* 🭋 */ + case 0x1fb61: /* 🭡 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = 0; p2_y = p3_y = height; + break; + + case 0x1fb57: /* 🭗 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb41: /* 🭁 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb58: /* 🭘 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb42: /* 🭂 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb59: /* 🭙 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb43: /* 🭃 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb5a: /* 🭚 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb44: /* 🭄 */ + p1_x = p2_x = 0; p3_x = width; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb5b: /* 🭛 */ + case 0x1fb45: /* 🭅 */ + p1_x = p2_x = 0; p3_x = halfs0; + p1_y = p3_y = 0; p2_y = height; + break; + + case 0x1fb62: /* 🭢 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb4c: /* 🭌 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb63: /* 🭣 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb4d: /* 🭍 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = p3_y = 0; p2_y = thirds0; + break; + + case 0x1fb64: /* 🭤 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb4e: /* 🭎 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb65: /* 🭥 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb4f: /* 🭏 */ + p1_x = p2_x = width; p3_x = 0; + p1_y = p3_y = 0; p2_y = thirds1; + break; + + case 0x1fb66: /* 🭦 */ + case 0x1fb50: /* 🭐 */ + p1_x = p2_x = width; p3_x = halfs1; + p1_y = p3_y = 0; p2_y = height; + break; + + case 0x1fb46: /* 🭆 */ + p1_x = 0; p1_y = thirds1; + p2_x = width; p2_y = thirds0; + p3_x = width; p3_y = p1_y; + break; + + case 0x1fb51: /* 🭑 */ + p1_x = 0; p1_y = thirds0; + p2_x = 0; p2_y = thirds1; + p3_x = width; p3_y = p2_y; + break; + + case 0x1fb5c: /* 🭜 */ + p1_x = 0; p1_y = thirds0; + p2_x = 0; p2_y = thirds1; + p3_x = width; p3_y = p1_y; + break; + + case 0x1fb67: /* 🭧 */ + p1_x = 0; p1_y = thirds0; + p2_x = width; p2_y = p1_y; + p3_x = width; p3_y = thirds1; + break; + + case 0x1fb6c: /* 🭬 */ + case 0x1fb68: /* 🭨 */ + p1_x = 0; p1_y = 0; + p2_x = halfs0; p2_y = height / 2; + p3_x = 0; p3_y = height; + break; + + case 0x1fb6d: /* 🭭 */ + case 0x1fb69: /* 🭩 */ + p1_x = 0; p1_y = 0; + p2_x = halfs1; p2_y = height / 2; + p3_x = width; p3_y = 0; + break; + + case 0x1fb6e: /* 🭮 */ + case 0x1fb6a: /* 🭪 */ + p1_x = width; p1_y = 0; + p2_x = halfs1; p2_y = height / 2; + p3_x = width; p3_y = height; + break; + + case 0x1fb6f: /* 🭯 */ + case 0x1fb6b: /* 🭫 */ + p1_x = 0; p1_y = height; + p2_x = halfs1; p2_y = height / 2; + p3_x = width; p3_y = height; + break; + + default: + BUG("unimplemented Unicode codepoint"); + break; + } + + const pixman_triangle_t tri = { + .p1 = {.x = pixman_int_to_fixed(p1_x), .y = pixman_int_to_fixed(p1_y)}, + .p2 = {.x = pixman_int_to_fixed(p2_x), .y = pixman_int_to_fixed(p2_y)}, + .p3 = {.x = pixman_int_to_fixed(p3_x), .y = pixman_int_to_fixed(p3_y)}, + }; + + pixman_image_t *src = pixman_image_create_solid_fill(&white); + pixman_composite_triangles( + PIXMAN_OP_OVER, src, buf->pix, buf->format, 0, 0, 0, 0, 1, &tri); + pixman_image_unref(src); +} + +static void NOINLINE +draw_wedge_triangle_inverted(struct buf *buf, char32_t wc) +{ + draw_wedge_triangle(buf, wc); + + pixman_image_t *src = pixman_image_create_solid_fill(&white); + pixman_image_composite(PIXMAN_OP_OUT, src, NULL, buf->pix, 0, 0, 0, 0, 0, 0, buf->width, buf->height); + pixman_image_unref(src); +} + +static void NOINLINE +draw_wedge_triangle_and_box(struct buf *buf, char32_t wc) +{ + draw_wedge_triangle(buf, wc); + + const int width = buf->width; + const int height = buf->height; + + pixman_box32_t box; + + switch (wc) { + case 0x1fb46: + case 0x1fb51: + box = (pixman_box32_t){ + .x1 = 0, .y1 = buf->y_thirds[1], + .x2 = width, .y2 = height, + }; + break; + + case 0x1fb5c: + case 0x1fb67: + box = (pixman_box32_t){ + .x1 = 0, .y1 = 0, + .x2 = width, .y2 = buf->y_thirds[0], + }; + break; + } + + pixman_image_fill_boxes(PIXMAN_OP_SRC, buf->pix, &white, 1, &box); +} + +static void +draw_left_and_lower_one_eighth_block(struct buf *buf) +{ + draw_left_one_eighth_block(buf); + draw_lower_one_eighth_block(buf); +} + +static void +draw_left_and_upper_one_eighth_block(struct buf *buf) +{ + draw_left_one_eighth_block(buf); + draw_upper_one_eighth_block(buf); +} + +static void +draw_right_and_upper_one_eighth_block(struct buf *buf) +{ + draw_right_one_eighth_block(buf); + draw_upper_one_eighth_block(buf); +} + +static void +draw_right_and_lower_one_eighth_block(struct buf *buf) +{ + draw_right_one_eighth_block(buf); + draw_lower_one_eighth_block(buf); +} + +static void +draw_upper_and_lower_one_eighth_block(struct buf *buf) +{ + draw_upper_one_eighth_block(buf); + draw_lower_one_eighth_block(buf); +} + +static void +draw_horizontal_one_eighth_1358_block(struct buf *buf) +{ + draw_upper_one_eighth_block(buf); + draw_horizontal_one_eighth_block_3(buf); + draw_horizontal_one_eighth_block_5(buf); + draw_lower_one_eighth_block(buf); +} + +static void +draw_right_one_quarter_block(struct buf *buf) +{ + rect(buf->width - round(buf->width / 4.), 0, buf->width, buf->height); +} + +static void +draw_right_three_eighths_block(struct buf *buf) +{ + rect(buf->width - round(3. * buf->width / 8.), 0, buf->width, buf->height); +} + +static void +draw_right_five_eighths_block(struct buf *buf) +{ + rect(buf->width - round(5. * buf->width / 8.), 0, buf->width, buf->height); +} + +static void +draw_right_three_quarters_block(struct buf *buf) +{ + rect(buf->width - round(3. * buf->width / 4.), 0, buf->width, buf->height); +} + +static void +draw_right_seven_eighths_block(struct buf *buf) +{ + rect(buf->width - round(7. * buf->width / 8.), 0, buf->width, buf->height); +} + +static void +draw_glyph(struct buf *buf, char32_t wc) +{ + IGNORE_WARNING("-Wpedantic") + + switch (wc) { + case 0x2500: draw_box_drawings_light_horizontal(buf); break; + case 0x2501: draw_box_drawings_heavy_horizontal(buf); break; + case 0x2502: draw_box_drawings_light_vertical(buf); break; + case 0x2503: draw_box_drawings_heavy_vertical(buf); break; + case 0x2504: draw_box_drawings_light_triple_dash_horizontal(buf); break; + case 0x2505: draw_box_drawings_heavy_triple_dash_horizontal(buf); break; + case 0x2506: draw_box_drawings_light_triple_dash_vertical(buf); break; + case 0x2507: draw_box_drawings_heavy_triple_dash_vertical(buf); break; + case 0x2508: draw_box_drawings_light_quadruple_dash_horizontal(buf); break; + case 0x2509: draw_box_drawings_heavy_quadruple_dash_horizontal(buf); break; + case 0x250a: draw_box_drawings_light_quadruple_dash_vertical(buf); break; + case 0x250b: draw_box_drawings_heavy_quadruple_dash_vertical(buf); break; + case 0x250c: draw_box_drawings_light_down_and_right(buf); break; + case 0x250d: draw_box_drawings_down_light_and_right_heavy(buf); break; + case 0x250e: draw_box_drawings_down_heavy_and_right_light(buf); break; + case 0x250f: draw_box_drawings_heavy_down_and_right(buf); break; + + case 0x2510: draw_box_drawings_light_down_and_left(buf); break; + case 0x2511: draw_box_drawings_down_light_and_left_heavy(buf); break; + case 0x2512: draw_box_drawings_down_heavy_and_left_light(buf); break; + case 0x2513: draw_box_drawings_heavy_down_and_left(buf); break; + case 0x2514: draw_box_drawings_light_up_and_right(buf); break; + case 0x2515: draw_box_drawings_up_light_and_right_heavy(buf); break; + case 0x2516: draw_box_drawings_up_heavy_and_right_light(buf); break; + case 0x2517: draw_box_drawings_heavy_up_and_right(buf); break; + case 0x2518: draw_box_drawings_light_up_and_left(buf); break; + case 0x2519: draw_box_drawings_up_light_and_left_heavy(buf); break; + case 0x251a: draw_box_drawings_up_heavy_and_left_light(buf); break; + case 0x251b: draw_box_drawings_heavy_up_and_left(buf); break; + case 0x251c: draw_box_drawings_light_vertical_and_right(buf); break; + case 0x251d: draw_box_drawings_vertical_light_and_right_heavy(buf); break; + case 0x251e: draw_box_drawings_up_heavy_and_right_down_light(buf); break; + case 0x251f: draw_box_drawings_down_heavy_and_right_up_light(buf); break; + + case 0x2520: draw_box_drawings_vertical_heavy_and_right_light(buf); break; + case 0x2521: draw_box_drawings_down_light_and_right_up_heavy(buf); break; + case 0x2522: draw_box_drawings_up_light_and_right_down_heavy(buf); break; + case 0x2523: draw_box_drawings_heavy_vertical_and_right(buf); break; + case 0x2524: draw_box_drawings_light_vertical_and_left(buf); break; + case 0x2525: draw_box_drawings_vertical_light_and_left_heavy(buf); break; + case 0x2526: draw_box_drawings_up_heavy_and_left_down_light(buf); break; + case 0x2527: draw_box_drawings_down_heavy_and_left_up_light(buf); break; + case 0x2528: draw_box_drawings_vertical_heavy_and_left_light(buf); break; + case 0x2529: draw_box_drawings_down_light_and_left_up_heavy(buf); break; + case 0x252a: draw_box_drawings_up_light_and_left_down_heavy(buf); break; + case 0x252b: draw_box_drawings_heavy_vertical_and_left(buf); break; + case 0x252c: draw_box_drawings_light_down_and_horizontal(buf); break; + case 0x252d: draw_box_drawings_left_heavy_and_right_down_light(buf); break; + case 0x252e: draw_box_drawings_right_heavy_and_left_down_light(buf); break; + case 0x252f: draw_box_drawings_down_light_and_horizontal_heavy(buf); break; + + case 0x2530: draw_box_drawings_down_heavy_and_horizontal_light(buf); break; + case 0x2531: draw_box_drawings_right_light_and_left_down_heavy(buf); break; + case 0x2532: draw_box_drawings_left_light_and_right_down_heavy(buf); break; + case 0x2533: draw_box_drawings_heavy_down_and_horizontal(buf); break; + case 0x2534: draw_box_drawings_light_up_and_horizontal(buf); break; + case 0x2535: draw_box_drawings_left_heavy_and_right_up_light(buf); break; + case 0x2536: draw_box_drawings_right_heavy_and_left_up_light(buf); break; + case 0x2537: draw_box_drawings_up_light_and_horizontal_heavy(buf); break; + case 0x2538: draw_box_drawings_up_heavy_and_horizontal_light(buf); break; + case 0x2539: draw_box_drawings_right_light_and_left_up_heavy(buf); break; + case 0x253a: draw_box_drawings_left_light_and_right_up_heavy(buf); break; + case 0x253b: draw_box_drawings_heavy_up_and_horizontal(buf); break; + case 0x253c: draw_box_drawings_light_vertical_and_horizontal(buf); break; + case 0x253d: draw_box_drawings_left_heavy_and_right_vertical_light(buf); break; + case 0x253e: draw_box_drawings_right_heavy_and_left_vertical_light(buf); break; + case 0x253f: draw_box_drawings_vertical_light_and_horizontal_heavy(buf); break; + + case 0x2540: draw_box_drawings_up_heavy_and_down_horizontal_light(buf); break; + case 0x2541: draw_box_drawings_down_heavy_and_up_horizontal_light(buf); break; + case 0x2542: draw_box_drawings_vertical_heavy_and_horizontal_light(buf); break; + case 0x2543: draw_box_drawings_left_up_heavy_and_right_down_light(buf); break; + case 0x2544: draw_box_drawings_right_up_heavy_and_left_down_light(buf); break; + case 0x2545: draw_box_drawings_left_down_heavy_and_right_up_light(buf); break; + case 0x2546: draw_box_drawings_right_down_heavy_and_left_up_light(buf); break; + case 0x2547: draw_box_drawings_down_light_and_up_horizontal_heavy(buf); break; + case 0x2548: draw_box_drawings_up_light_and_down_horizontal_heavy(buf); break; + case 0x2549: draw_box_drawings_right_light_and_left_vertical_heavy(buf); break; + case 0x254a: draw_box_drawings_left_light_and_right_vertical_heavy(buf); break; + case 0x254b: draw_box_drawings_heavy_vertical_and_horizontal(buf); break; + case 0x254c: draw_box_drawings_light_double_dash_horizontal(buf); break; + case 0x254d: draw_box_drawings_heavy_double_dash_horizontal(buf); break; + case 0x254e: draw_box_drawings_light_double_dash_vertical(buf); break; + case 0x254f: draw_box_drawings_heavy_double_dash_vertical(buf); break; + + case 0x2550: draw_box_drawings_double_horizontal(buf); break; + case 0x2551: draw_box_drawings_double_vertical(buf); break; + case 0x2552: draw_box_drawings_down_single_and_right_double(buf); break; + case 0x2553: draw_box_drawings_down_double_and_right_single(buf); break; + case 0x2554: draw_box_drawings_double_down_and_right(buf); break; + case 0x2555: draw_box_drawings_down_single_and_left_double(buf); break; + case 0x2556: draw_box_drawings_down_double_and_left_single(buf); break; + case 0x2557: draw_box_drawings_double_down_and_left(buf); break; + case 0x2558: draw_box_drawings_up_single_and_right_double(buf); break; + case 0x2559: draw_box_drawings_up_double_and_right_single(buf); break; + case 0x255a: draw_box_drawings_double_up_and_right(buf); break; + case 0x255b: draw_box_drawings_up_single_and_left_double(buf); break; + case 0x255c: draw_box_drawings_up_double_and_left_single(buf); break; + case 0x255d: draw_box_drawings_double_up_and_left(buf); break; + case 0x255e: draw_box_drawings_vertical_single_and_right_double(buf); break; + case 0x255f: draw_box_drawings_vertical_double_and_right_single(buf); break; + + case 0x2560: draw_box_drawings_double_vertical_and_right(buf); break; + case 0x2561: draw_box_drawings_vertical_single_and_left_double(buf); break; + case 0x2562: draw_box_drawings_vertical_double_and_left_single(buf); break; + case 0x2563: draw_box_drawings_double_vertical_and_left(buf); break; + case 0x2564: draw_box_drawings_down_single_and_horizontal_double(buf); break; + case 0x2565: draw_box_drawings_down_double_and_horizontal_single(buf); break; + case 0x2566: draw_box_drawings_double_down_and_horizontal(buf); break; + case 0x2567: draw_box_drawings_up_single_and_horizontal_double(buf); break; + case 0x2568: draw_box_drawings_up_double_and_horizontal_single(buf); break; + case 0x2569: draw_box_drawings_double_up_and_horizontal(buf); break; + case 0x256a: draw_box_drawings_vertical_single_and_horizontal_double(buf); break; + case 0x256b: draw_box_drawings_vertical_double_and_horizontal_single(buf); break; + case 0x256c: draw_box_drawings_double_vertical_and_horizontal(buf); break; + case 0x256d ... 0x2570: draw_box_drawings_light_arc(buf, wc); break; + + case 0x2571: draw_box_drawings_light_diagonal_upper_right_to_lower_left(buf); break; + case 0x2572: draw_box_drawings_light_diagonal_upper_left_to_lower_right(buf); break; + case 0x2573: draw_box_drawings_light_diagonal_cross(buf); break; + case 0x2574: draw_box_drawings_light_left(buf); break; + case 0x2575: draw_box_drawings_light_up(buf); break; + case 0x2576: draw_box_drawings_light_right(buf); break; + case 0x2577: draw_box_drawings_light_down(buf); break; + case 0x2578: draw_box_drawings_heavy_left(buf); break; + case 0x2579: draw_box_drawings_heavy_up(buf); break; + case 0x257a: draw_box_drawings_heavy_right(buf); break; + case 0x257b: draw_box_drawings_heavy_down(buf); break; + case 0x257c: draw_box_drawings_light_left_and_heavy_right(buf); break; + case 0x257d: draw_box_drawings_light_up_and_heavy_down(buf); break; + case 0x257e: draw_box_drawings_heavy_left_and_light_right(buf); break; + case 0x257f: draw_box_drawings_heavy_up_and_light_down(buf); break; + + case 0x2580: draw_upper_half_block(buf); break; + case 0x2581: draw_lower_one_eighth_block(buf); break; + case 0x2582: draw_lower_one_quarter_block(buf); break; + case 0x2583: draw_lower_three_eighths_block(buf); break; + case 0x2584: draw_lower_half_block(buf); break; + case 0x2585: draw_lower_five_eighths_block(buf); break; + case 0x2586: draw_lower_three_quarters_block(buf); break; + case 0x2587: draw_lower_seven_eighths_block(buf); break; + case 0x2588: draw_full_block(buf); break; + case 0x2589: draw_left_seven_eighths_block(buf); break; + case 0x258a: draw_left_three_quarters_block(buf); break; + case 0x258b: draw_left_five_eighths_block(buf); break; + case 0x258c: draw_left_half_block(buf); break; + case 0x258d: draw_left_three_eighths_block(buf); break; + case 0x258e: draw_left_one_quarter_block(buf); break; + case 0x258f: draw_left_one_eighth_block(buf); break; + + case 0x2590: draw_right_half_block(buf); break; + case 0x2591: draw_light_shade(buf); break; + case 0x2592: draw_medium_shade(buf); break; + case 0x2593: draw_dark_shade(buf); break; + case 0x2594: draw_upper_one_eighth_block(buf); break; + case 0x2595: draw_right_one_eighth_block(buf); break; + case 0x2596 ... 0x259f: draw_quadrant(buf, wc); break; + + case 0x2800 ... 0x28ff: draw_braille(buf, wc); break; + + case 0x1cd00 ... 0x1cde5: draw_octant(buf, wc); break; + case 0x1fb00 ... 0x1fb3b: draw_sextant(buf, wc); break; + + case 0x1fb3c ... 0x1fb40: + case 0x1fb47 ... 0x1fb4b: + case 0x1fb57 ... 0x1fb5b: + case 0x1fb62 ... 0x1fb66: + case 0x1fb6c ... 0x1fb6f: + draw_wedge_triangle(buf, wc); + break; + + case 0x1fb41 ... 0x1fb45: + case 0x1fb4c ... 0x1fb50: + case 0x1fb52 ... 0x1fb56: + case 0x1fb5d ... 0x1fb61: + case 0x1fb68 ... 0x1fb6b: + draw_wedge_triangle_inverted(buf, wc); + break; + + case 0x1fb46: + case 0x1fb51: + case 0x1fb5c: + case 0x1fb67: + draw_wedge_triangle_and_box(buf, wc); + break; + + case 0x1fb9a: + draw_wedge_triangle(buf, 0x1fb6d); + draw_wedge_triangle(buf, 0x1fb6f); + break; + + case 0x1fb9b: + draw_wedge_triangle(buf, 0x1fb6c); + draw_wedge_triangle(buf, 0x1fb6e); + break; + + case 0x1fb70: draw_vertical_one_eighth_block_2(buf); break; + case 0x1fb71: draw_vertical_one_eighth_block_3(buf); break; + case 0x1fb72: draw_vertical_one_eighth_block_4(buf); break; + case 0x1fb73: draw_vertical_one_eighth_block_5(buf); break; + case 0x1fb74: draw_vertical_one_eighth_block_6(buf); break; + case 0x1fb75: draw_vertical_one_eighth_block_7(buf); break; + + case 0x1fb76: draw_horizontal_one_eighth_block_2(buf); break; + case 0x1fb77: draw_horizontal_one_eighth_block_3(buf); break; + case 0x1fb78: draw_horizontal_one_eighth_block_4(buf); break; + case 0x1fb79: draw_horizontal_one_eighth_block_5(buf); break; + case 0x1fb7a: draw_horizontal_one_eighth_block_6(buf); break; + case 0x1fb7b: draw_horizontal_one_eighth_block_7(buf); break; + + case 0x1fb82: draw_upper_one_quarter_block(buf); break; + case 0x1fb83: draw_upper_three_eighths_block(buf); break; + case 0x1fb84: draw_upper_five_eighths_block(buf); break; + case 0x1fb85: draw_upper_three_quarters_block(buf); break; + case 0x1fb86: draw_upper_seven_eighths_block(buf); break; + + case 0x1fb7c: draw_left_and_lower_one_eighth_block(buf); break; + case 0x1fb7d: draw_left_and_upper_one_eighth_block(buf); break; + case 0x1fb7e: draw_right_and_upper_one_eighth_block(buf); break; + case 0x1fb7f: draw_right_and_lower_one_eighth_block(buf); break; + case 0x1fb80: draw_upper_and_lower_one_eighth_block(buf); break; + case 0x1fb81: draw_horizontal_one_eighth_1358_block(buf); break; + + case 0x1fb87: draw_right_one_quarter_block(buf); break; + case 0x1fb88: draw_right_three_eighths_block(buf); break; + case 0x1fb89: draw_right_five_eighths_block(buf); break; + case 0x1fb8a: draw_right_three_quarters_block(buf); break; + case 0x1fb8b: draw_right_seven_eighths_block(buf); break; + } + + UNIGNORE_WARNINGS +} + +struct fcft_glyph * COLD +box_drawing(const struct terminal *term, char32_t wc) +{ + int width = term->cell_width; + int height = term->cell_height; + + pixman_format_code_t fmt = + term->fonts[0]->antialias ? PIXMAN_a8 : PIXMAN_a1; + + int stride = stride_for_format_and_width(fmt, width); + uint8_t *data = xcalloc(height * stride, 1); + + pixman_image_t *pix = pixman_image_create_bits_no_clear( + fmt, width, height, (uint32_t*)data, stride); + + if (pix == NULL) { + errno = ENOMEM; + perror(__func__); + abort(); + } + + double dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + double scale = term->font_is_sized_by_dpi ? 1. : term->scale; + double cell_size = sqrt(pow(term->cell_width, 2) + pow(term->cell_height, 2)); + + int base_thickness = + (double)term->conf->tweak.box_drawing_base_thickness * scale * cell_size * dpi / 72.0; + base_thickness = max(base_thickness, 1); + + int y_third_0 = 0, y_third_1 = 0; + switch (height % 3) { + case 0: + y_third_0 = height / 3; + y_third_1 = 2 * height / 3; + break; + + case 1: + y_third_0 = height / 3; + y_third_1 = 2 * height / 3 + 1; + break; + + case 2: + y_third_0 = height / 3 + 1; + y_third_1 = y_third_0 + height / 3; + break; + } + + /* TODO */ + int y_quad_0 = 0, y_quad_1 = 0, y_quad_2 = 0; + switch (height % 4) { + case 0: + y_quad_0 = height / 4; + y_quad_1 = height / 2; + y_quad_2 = 3 * height / 4; + break; + + case 1: + y_quad_0 = height / 4; + y_quad_1 = height / 2; + y_quad_2 = 3 * height / 4; + break; + case 2: + y_quad_0 = height / 4; + y_quad_1 = height / 2; + y_quad_2 = 3 * height / 4; + break; + + case 3: + y_quad_0 = height / 4; + y_quad_1 = height / 2; + y_quad_2 = 3 * height / 4; + break; + } + + struct buf buf = { + .data = data, + .pix = pix, + .format = fmt, + .width = width, + .height = height, + .stride = stride, + .solid_shades = term->conf->tweak.box_drawing_solid_shades, + + .thickness = { + [LIGHT] = _thickness(base_thickness, LIGHT), + [HEAVY] = _thickness(base_thickness, HEAVY), + }, + + /* Overlap when width is odd */ + .x_halfs = { + round(width / 2.), /* Endpoint first half */ + width / 2, /* Startpoint second half */ + }, + + .y_thirds = { + y_third_0, /* Endpoint first third, start point second third */ + y_third_1, /* Endpoint second third, start point last third */ + }, + + .y_quads = { + y_quad_0, + y_quad_1, + y_quad_2, + }, + }; + + LOG_DBG("LIGHT=%d, HEAVY=%d", buf.thickness[LIGHT], buf.thickness[HEAVY]); + + draw_glyph(&buf, wc); + + struct fcft_glyph *glyph = xmalloc(sizeof(*glyph)); + *glyph = (struct fcft_glyph){ + .cp = wc, + .cols = 1, + .pix = buf.pix, + .x = -term->font_x_ofs, + .y = term->font_baseline, + .width = width, + .height = height, + .advance = { + .x = width, + .y = height, + }, + }; + return glyph; +} diff --git a/box-drawing.h b/box-drawing.h new file mode 100644 index 0000000..d7413bd --- /dev/null +++ b/box-drawing.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include + +struct terminal; +struct fcft_glyph *box_drawing(const struct terminal *term, char32_t wc); diff --git a/char32.c b/char32.c new file mode 100644 index 0000000..be5bf22 --- /dev/null +++ b/char32.c @@ -0,0 +1,432 @@ +#include "char32.h" + +#include +#include +#include + +#include +#include + +#if defined __has_include + #if __has_include () + #include + #endif +#endif + +#define LOG_MODULE "char32" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" +#include "macros.h" +#include "xmalloc.h" + +/* + * For now, assume we can map directly to the corresponding wchar_t + * functions. This is true if: + * + * - both data types have the same size + * - both use the same encoding (though we require that encoding to be UTF-32) + */ + +_Static_assert( + sizeof(wchar_t) == sizeof(char32_t), "wchar_t vs. char32_t size mismatch"); + +#if !defined(__STDC_UTF_32__) || !__STDC_UTF_32__ + #error "char32_t does not use UTF-32" +#endif +#if (!defined(__STDC_ISO_10646__) || !__STDC_ISO_10646__) && !defined(__FreeBSD__) && !defined(__OpenBSD__) + #error "wchar_t does not use UTF-32" +#endif + +UNITTEST +{ + xassert(c32len(U"") == 0); + xassert(c32len(U"foobar") == 6); +} + +UNITTEST +{ + xassert(c32cmp(U"foobar", U"foobar") == 0); + xassert(c32cmp(U"foo", U"foobar") < 0); + xassert(c32cmp(U"foobar", U"foo") > 0); + xassert(c32cmp(U"a", U"b") < 0); + xassert(c32cmp(U"b", U"a") > 0); +} + +UNITTEST +{ + xassert(c32ncmp(U"foo", U"foot", 3) == 0); + xassert(c32ncmp(U"foot", U"FOOT", 4) > 0); + xassert(c32ncmp(U"a", U"b", 1) < 0); + xassert(c32ncmp(U"bb", U"aa", 2) > 0); +} + +UNITTEST +{ + char32_t copy[16]; + char32_t *ret = c32ncpy(copy, U"foobar", 16); + + xassert(ret == copy); + xassert(copy[0] == U'f'); + xassert(copy[1] == U'o'); + xassert(copy[2] == U'o'); + xassert(copy[3] == U'b'); + xassert(copy[4] == U'a'); + xassert(copy[5] == U'r'); + + unsigned char zeroes[(16 - 6) * sizeof(copy[0])] = {0}; + xassert(memcmp(©[6], zeroes, sizeof(zeroes)) == 0); +} + +UNITTEST +{ + char32_t copy[16]; + memset(copy, 0x55, sizeof(copy)); + + char32_t *ret = c32cpy(copy, U"foobar"); + + xassert(ret == copy); + xassert(copy[0] == U'f'); + xassert(copy[1] == U'o'); + xassert(copy[2] == U'o'); + xassert(copy[3] == U'b'); + xassert(copy[4] == U'a'); + xassert(copy[5] == U'r'); + xassert(copy[6] == U'\0'); + + unsigned char fives[(16 - 6 - 1) * sizeof(copy[0])]; + memset(fives, 0x55, sizeof(fives)); + xassert(memcmp(©[7], fives, sizeof(fives)) == 0); +} + +UNITTEST +{ + xassert(c32casecmp(U"foobar", U"FOOBAR") == 0); + xassert(c32casecmp(U"foo", U"FOOO") < 0); + xassert(c32casecmp(U"FOOO", U"foo") > 0); + xassert(c32casecmp(U"a", U"B") < 0); + xassert(c32casecmp(U"B", U"a") > 0); +} + +UNITTEST +{ + xassert(c32ncasecmp(U"foo", U"FOObar", 3) == 0); + xassert(c32ncasecmp(U"foo", U"FOOO", 4) < 0); + xassert(c32ncasecmp(U"FOOO", U"foo", 4) > 0); + xassert(c32ncasecmp(U"a", U"BB", 1) < 0); + xassert(c32ncasecmp(U"BB", U"a", 1) > 0); +} + +UNITTEST +{ + char32_t dst[32] = U"foobar"; + char32_t *ret = c32ncat(dst, U"12345678XXXXXXXXX", 8); + + xassert(ret == dst); + xassert(c32cmp(dst, U"foobar12345678") == 0); +} + +UNITTEST +{ + char32_t dst[32] = U"foobar"; + char32_t *ret = c32cat(dst, U"12345678"); + + xassert(ret == dst); + xassert(c32cmp(dst, U"foobar12345678") == 0); +} + +UNITTEST +{ + xassert(!isc32upper(U'a')); + xassert(isc32upper(U'A')); + xassert(!isc32upper(U'a')); +} + +UNITTEST +{ + xassert(hasc32upper(U"abc1A")); + xassert(!hasc32upper(U"abc1_aaa")); + xassert(!hasc32upper(U"")); +} + +UNITTEST +{ + char32_t *c = xc32dup(U"foobar"); + xassert(c32cmp(c, U"foobar") == 0); + free(c); + + c = xc32dup(U""); + xassert(c32cmp(c, U"") == 0); + free(c); +} + +size_t +mbsntoc32(char32_t *dst, const char *src, size_t nms, size_t len) +{ + mbstate_t ps = {0}; + + char32_t *out = dst; + const char *in = src; + + size_t consumed = 0; + size_t chars = 0; + size_t rc; + + while ((out == NULL || chars < len) && + consumed < nms && + (rc = mbrtoc32(out, in, nms - consumed, &ps)) != 0) + { + switch (rc) { + case 0: + goto done; + + case (size_t)-1: + case (size_t)-2: + case (size_t)-3: + goto err; + } + + in += rc; + consumed += rc; + chars++; + + if (out != NULL) + out++; + } + +done: + return chars; + +err: + return (size_t)-1; +} + +UNITTEST +{ + const char input[] = "foobarzoo"; + char32_t c32[32]; + + size_t ret = mbsntoc32(NULL, input, sizeof(input), 0); + xassert(ret == 9); + + memset(c32, 0x55, sizeof(c32)); + ret = mbsntoc32(c32, input, sizeof(input), 32); + + xassert(ret == 9); + xassert(c32[0] == U'f'); + xassert(c32[1] == U'o'); + xassert(c32[2] == U'o'); + xassert(c32[3] == U'b'); + xassert(c32[4] == U'a'); + xassert(c32[5] == U'r'); + xassert(c32[6] == U'z'); + xassert(c32[7] == U'o'); + xassert(c32[8] == U'o'); + xassert(c32[9] == U'\0'); + xassert(c32[10] == 0x55555555); + + memset(c32, 0x55, sizeof(c32)); + ret = mbsntoc32(c32, input, 1, 32); + + xassert(ret == 1); + xassert(c32[0] == U'f'); + xassert(c32[1] == 0x55555555); + + memset(c32, 0x55, sizeof(c32)); + ret = mbsntoc32(c32, input, sizeof(input), 1); + + xassert(ret == 1); + xassert(c32[0] == U'f'); + xassert(c32[1] == 0x55555555); +} + +UNITTEST +{ + const char input[] = "foobarzoo"; + char32_t c32[32]; + + size_t ret = mbstoc32(NULL, input, 0); + xassert(ret == 9); + + memset(c32, 0x55, sizeof(c32)); + ret = mbstoc32(c32, input, 32); + + xassert(ret == 9); + xassert(c32[0] == U'f'); + xassert(c32[1] == U'o'); + xassert(c32[2] == U'o'); + xassert(c32[3] == U'b'); + xassert(c32[4] == U'a'); + xassert(c32[5] == U'r'); + xassert(c32[6] == U'z'); + xassert(c32[7] == U'o'); + xassert(c32[8] == U'o'); + xassert(c32[9] == U'\0'); + xassert(c32[10] == 0x55555555); + + memset(c32, 0x55, sizeof(c32)); + ret = mbstoc32(c32, input, 1); + + xassert(ret == 1); + xassert(c32[0] == U'f'); + xassert(c32[1] == 0x55555555); +} + + +char32_t * +ambstoc32(const char *src) +{ + if (src == NULL) + return NULL; + + const size_t src_len = strlen(src); + + char32_t *ret = xmalloc((src_len + 1) * sizeof(ret[0])); + mbstate_t ps = {0}; + + char32_t *out = ret; + const char *in = src; + const char *const end = src + src_len + 1; + + size_t chars = 0; + size_t rc; + + while ((rc = mbrtoc32(out, in, end - in, &ps)) != 0) { + switch (rc) { + case (size_t)-1: + case (size_t)-2: + case (size_t)-3: + goto err; + } + + in += rc; + out++; + chars++; + } + + *out = U'\0'; + + ret = xrealloc(ret, (chars + 1) * sizeof(ret[0])); + return ret; + +err: + free(ret); + return NULL; +} + +UNITTEST +{ + const char* locale = setlocale(LC_CTYPE, "en_US.UTF-8"); + if (!locale) + locale = setlocale(LC_CTYPE, "C.UTF-8"); + if (!locale) + return; + + char32_t *hello = ambstoc32(u8"hello"); + xassert(hello != NULL); + xassert(hello[0] == U'h'); + xassert(hello[1] == U'e'); + xassert(hello[2] == U'l'); + xassert(hello[3] == U'l'); + xassert(hello[4] == U'o'); + xassert(hello[5] == U'\0'); + free(hello); + + char32_t *swedish = ambstoc32(u8"åäö"); + xassert(swedish != NULL); + xassert(swedish[0] == U'å'); + xassert(swedish[1] == U'ä'); + xassert(swedish[2] == U'ö'); + xassert(swedish[3] == U'\0'); + free(swedish); + + char32_t *emoji = ambstoc32(u8"👨‍👩‍👧‍👦"); + xassert(emoji != NULL); + xassert(emoji[0] == U'👨'); + xassert(emoji[1] == U'‍'); + xassert(emoji[2] == U'👩'); + xassert(emoji[3] == U'‍'); + xassert(emoji[4] == U'👧'); + xassert(emoji[5] == U'‍'); + xassert(emoji[6] == U'👦'); + xassert(emoji[7] == U'\0'); + free(emoji); + + xassert(ambstoc32(NULL) == NULL); + xassert(setlocale(LC_CTYPE, "C") != NULL); +} + +char * +ac32tombs(const char32_t *src) +{ + if (src == NULL) + return NULL; + + const size_t src_len = c32len(src); + + size_t allocated = src_len + 1; + char *ret = xmalloc(allocated); + mbstate_t ps = {0}; + + char *out = ret; + const char32_t *const end = src + src_len + 1; + + size_t bytes = 0; + + char mb[MB_CUR_MAX]; + + for (const char32_t *in = src; in < end; in++) { + size_t rc = c32rtomb(mb, *in, &ps); + + switch (rc) { + case (size_t)-1: + goto err; + } + + if (bytes + rc > allocated) { + allocated *= 2; + ret = xrealloc(ret, allocated); + out = &ret[bytes]; + } + + for (size_t i = 0; i < rc; i++, out++) + *out = mb[i]; + + bytes += rc; + } + + xassert(ret[bytes - 1] == '\0'); + ret = xrealloc(ret, bytes); + return ret; + +err: + free(ret); + return NULL; +} + +UNITTEST +{ + const char* locale = setlocale(LC_CTYPE, "en_US.UTF-8"); + if (!locale) + locale = setlocale(LC_CTYPE, "C.UTF-8"); + if (!locale) + return; + + char *s = ac32tombs(U"foobar"); + xassert(s != NULL); + xassert(strcmp(s, "foobar") == 0); + free(s); + + s = ac32tombs(U"åäö"); + xassert(s != NULL); + xassert(strcmp(s, u8"åäö") == 0); + free(s); + + s = ac32tombs(U"👨‍👩‍👧‍👦"); + xassert(s != NULL); + xassert(strcmp(s, u8"👨‍👩‍👧‍👦") == 0); + free(s); + + xassert(ac32tombs(NULL) == NULL); + xassert(setlocale(LC_CTYPE, "C") != NULL); +} diff --git a/char32.h b/char32.h new file mode 100644 index 0000000..dcb412c --- /dev/null +++ b/char32.h @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#if defined(FOOT_GRAPHEME_CLUSTERING) + #include +#endif + +static inline size_t c32len(const char32_t *s) { + return wcslen((const wchar_t *)s); +} + +static inline int c32cmp(const char32_t *s1, const char32_t *s2) { + return wcscmp((const wchar_t *)s1, (const wchar_t *)s2); +} + +static inline int c32ncmp(const char32_t *s1, const char32_t *s2, size_t n) { + return wcsncmp((const wchar_t *)s1, (const wchar_t *)s2, n); +} + +static inline char32_t *c32ncpy(char32_t *dst, const char32_t *src, size_t n) { + return (char32_t *)wcsncpy((wchar_t *)dst, (const wchar_t *)src, n); +} + +static inline char32_t *c32cpy(char32_t *dst, const char32_t *src) { + return (char32_t *)wcscpy((wchar_t *)dst, (const wchar_t *)src); +} + +static inline char32_t *c32ncat(char32_t *dst, const char32_t *src, size_t n) { + return (char32_t *)wcsncat((wchar_t *)dst, (const wchar_t *)src, n); +} + +static inline char32_t *c32cat(char32_t *dst, const char32_t *src) { + return (char32_t *)wcscat((wchar_t *)dst, (const wchar_t *)src); +} + +static inline char32_t *c32dup(const char32_t *s) { + return (char32_t *)wcsdup((const wchar_t *)s); +} + +static inline char32_t *c32chr(const char32_t *s, char32_t c) { + return (char32_t *)wcschr((const wchar_t *)s, c); +} + +static inline int c32casecmp(const char32_t *s1, const char32_t *s2) { + return wcscasecmp((const wchar_t *)s1, (const wchar_t *)s2); +} + +static inline int c32ncasecmp(const char32_t *s1, const char32_t *s2, size_t n) { + return wcsncasecmp((const wchar_t *)s1, (const wchar_t *)s2, n); +} + +static inline char32_t toc32lower(char32_t c) { + return (char32_t)towlower((wint_t)c); +} + +static inline char32_t toc32upper(char32_t c) { + return (char32_t)towupper((wint_t)c); +} + +static inline bool isc32upper(char32_t c32) { + return iswupper((wint_t)c32); +} + +static inline bool isc32space(char32_t c32) { + return iswspace((wint_t)c32); +} + +static inline bool isc32print(char32_t c32) { + return iswprint((wint_t)c32); +} + +static inline bool isc32graph(char32_t c32) { + return iswgraph((wint_t)c32); +} + +static inline bool hasc32upper(const char32_t *s) { + for (int i = 0; s[i] != '\0'; i++) { + if (isc32upper(s[i])) return true; + } + return false; +} + +static inline int c32width(char32_t c) { +#if defined(FOOT_GRAPHEME_CLUSTERING) + return utf8proc_charwidth((utf8proc_int32_t)c); +#else + return wcwidth((wchar_t)c); +#endif +} + +static inline int c32swidth(const char32_t *s, size_t n) { +#if defined(FOOT_GRAPHEME_CLUSTERING) + int width = 0; + for (size_t i = 0; i < n; i++) + width += utf8proc_charwidth((utf8proc_int32_t)s[i]); + return width; +#else + return wcswidth((const wchar_t *)s, n); +#endif +} + +size_t mbsntoc32(char32_t *dst, const char *src, size_t nms, size_t len); +char32_t *ambstoc32(const char *src); +char *ac32tombs(const char32_t *src); + +static inline size_t mbstoc32(char32_t *dst, const char *src, size_t len) { + return mbsntoc32(dst, src, strlen(src) + 1, len); +} diff --git a/client-protocol.h b/client-protocol.h new file mode 100644 index 0000000..efd601d --- /dev/null +++ b/client-protocol.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +struct client_string { + uint16_t len; + /* char str[static len]; */ +}; + +struct client_data { + bool hold:1; + bool no_wait:1; + bool xdga_token:1; + uint8_t reserved:5; + + uint8_t token_len; + uint16_t cwd_len; + uint16_t override_count; + uint16_t argc; + uint16_t env_count; + + /* char cwd[static cwd_len]; */ + /* char token[static token_len]; */ + /* struct client_string overrides[static override_count]; */ + /* struct client_string argv[static argc]; */ + /* struct client_string envp[static env_count]; */ +} __attribute__((packed)); + +_Static_assert(sizeof(struct client_data) == 10, "protocol struct size error"); + +enum client_ipc_code { + FOOT_IPC_SIGUSR, +}; + +struct client_ipc_hdr { + enum client_ipc_code ipc_code; + uint8_t size; +} __attribute__((packed)); + + +struct client_ipc_sigusr { + int signo; +} __attribute__((packed)); diff --git a/client.c b/client.c new file mode 100644 index 0000000..befd3ab --- /dev/null +++ b/client.c @@ -0,0 +1,604 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#define LOG_MODULE "foot-client" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "client-protocol.h" +#include "debug.h" +#include "foot-features.h" +#include "macros.h" +#include "util.h" +#include "xmalloc.h" + +extern char **environ; + +struct string { + size_t len; + char *str; +}; +typedef tll(struct string) string_list_t; + +static volatile sig_atomic_t aborted = 0; +static volatile sig_atomic_t sigusr = 0; + +static void +sigint_handler(int signo) +{ + aborted = 1; +} + +static void +sigusr_handler(int signo) +{ + sigusr = signo; +} + +static ssize_t +sendall(int sock, const void *_buf, size_t len) +{ + const uint8_t *buf = _buf; + size_t left = len; + + while (left > 0) { + ssize_t r = send(sock, buf, left, MSG_NOSIGNAL); + if (r < 0) { + if (errno == EINTR) + continue; + return r; + } + + buf += r; + left -= r; + } + + return len; +} + +static void +print_usage(const char *prog_name) +{ + static const char options[] = + "\nOptions:\n" + " -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n" + " -T,--title=TITLE initial window title (foot)\n" + " -a,--app-id=ID window application ID (foot)\n" + " --toplevel-tag=TAG set a custom toplevel tag\n" + " -w,--window-size-pixels=WIDTHxHEIGHT initial width and height, in pixels\n" + " -W,--window-size-chars=WIDTHxHEIGHT initial width and height, in characters\n" + " -m,--maximized start in maximized mode\n" + " -F,--fullscreen start in fullscreen mode\n" + " -L,--login-shell start shell as a login shell\n" + " -D,--working-directory=DIR directory to start in (CWD)\n" + " -s,--server-socket=PATH path to the server UNIX domain socket (default=$XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock)\n" + " -H,--hold remain open after child process exits\n" + " -N,--no-wait detach the client process from the running terminal, exiting immediately\n" + " -o,--override=[section.]key=value override configuration option\n" + " -E, --client-environment exec shell using footclient's environment, instead of the server's\n" + " -d,--log-level={info|warning|error|none} log level (warning)\n" + " -l,--log-colorize=[{never|always|auto}] enable/disable colorization of log output on stderr\n" + " -v,--version show the version number and quit\n" + " -e ignored (for compatibility with xterm -e)\n"; + + printf("Usage: %s [OPTIONS...]\n", prog_name); + printf("Usage: %s [OPTIONS...] command [ARGS...]\n", prog_name); + puts(options); +} + +static bool NOINLINE +push_string(string_list_t *string_list, const char *s, uint64_t *total_len) +{ + size_t len = strlen(s) + 1; + if (len >= 1 << (8 * sizeof(uint16_t))) { + LOG_ERR("string length overflow"); + return false; + } + + struct string o = {len, xstrdup(s)}; + tll_push_back(*string_list, o); + *total_len += sizeof(struct client_string) + o.len; + return true; +} + +static void +free_string_list(string_list_t *string_list) +{ + tll_foreach(*string_list, it) { + free(it->item.str); + tll_remove(*string_list, it); + } +} + +static bool +send_string_list(int fd, const string_list_t *string_list) +{ + tll_foreach(*string_list, it) { + const struct client_string s = {it->item.len}; + if (sendall(fd, &s, sizeof(s)) < 0 || + sendall(fd, it->item.str, s.len) < 0) + { + LOG_ERRNO("failed to send setup packet to server"); + return false; + } + } + + return true; +} + +enum { + TOPLEVEL_TAG_OPTION = CHAR_MAX + 1, +}; + +int +main(int argc, char *const *argv) +{ + /* Custom exit code, to enable users to differentiate between foot + * itself failing, and the client application failing */ + static const int foot_exit_failure = -36; + int ret = foot_exit_failure; + + const char *const prog_name = argc > 0 ? argv[0] : ""; + + static const struct option longopts[] = { + {"term", required_argument, NULL, 't'}, + {"title", required_argument, NULL, 'T'}, + {"app-id", required_argument, NULL, 'a'}, + {"toplevel-tag", required_argument, NULL, TOPLEVEL_TAG_OPTION}, + {"window-size-pixels", required_argument, NULL, 'w'}, + {"window-size-chars", required_argument, NULL, 'W'}, + {"maximized", no_argument, NULL, 'm'}, + {"fullscreen", no_argument, NULL, 'F'}, + {"login-shell", no_argument, NULL, 'L'}, + {"working-directory", required_argument, NULL, 'D'}, + {"server-socket", required_argument, NULL, 's'}, + {"hold", no_argument, NULL, 'H'}, + {"no-wait", no_argument, NULL, 'N'}, + {"override", required_argument, NULL, 'o'}, + {"client-environment", no_argument, NULL, 'E'}, + {"log-level", required_argument, NULL, 'd'}, + {"log-colorize", optional_argument, NULL, 'l'}, + {"version", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, + {NULL, no_argument, NULL, 0}, + }; + + const char *custom_cwd = NULL; + const char *server_socket_path = NULL; + enum log_class log_level = LOG_CLASS_WARNING; + enum log_colorize log_colorize = LOG_COLORIZE_AUTO; + bool hold = false; + bool client_environment = false; + + /* Used to format overrides */ + bool no_wait = false; + + /* For XDG activation */ + const char *token = getenv("XDG_ACTIVATION_TOKEN"); + bool xdga_token = token != NULL; + size_t token_len = xdga_token ? strlen(token) + 1 : 0; + + char buf[1024]; + + /* Total packet length, not (yet) including overrides or argv[] */ + uint64_t total_len = 0; + + /* malloc:ed and needs to be in scope of all goto's */ + int fd = -1; + char *_cwd = NULL; + struct client_string *cargv = NULL; + string_list_t overrides = tll_init(); + string_list_t envp = tll_init(); + + while (true) { + int c = getopt_long(argc, argv, "+t:T:a:w:W:mFLD:s:HNo:Ed:l::veh", longopts, NULL); + if (c == -1) + break; + + switch (c) { + case 't': + snprintf(buf, sizeof(buf), "term=%s", optarg); + if (!push_string(&overrides, buf, &total_len)) + goto err; + break; + + case 'T': + snprintf(buf, sizeof(buf), "title=%s", optarg); + if (!push_string(&overrides, buf, &total_len)) + goto err; + break; + + case 'a': + snprintf(buf, sizeof(buf), "app-id=%s", optarg); + if (!push_string(&overrides, buf, &total_len)) + goto err; + break; + + case TOPLEVEL_TAG_OPTION: + snprintf(buf, sizeof(buf), "toplevel-tag=%s", optarg); + if (!push_string(&overrides, buf, &total_len)) + goto err; + break; + + case 'L': + if (!push_string(&overrides, "login-shell=yes", &total_len)) + goto err; + break; + + case 'D': { + struct stat st; + if (stat(optarg, &st) < 0 || !(st.st_mode & S_IFDIR)) { + fprintf(stderr, "error: %s: not a directory\n", optarg); + goto err; + } + custom_cwd = optarg; + break; + } + + case 'w': { + unsigned width, height; + if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) { + fprintf(stderr, "error: invalid window-size-pixels: %s\n", optarg); + goto err; + } + + snprintf(buf, sizeof(buf), "initial-window-size-pixels=%ux%u", width, height); + if (!push_string(&overrides, buf, &total_len)) + goto err; + break; + } + + case 'W': { + unsigned width, height; + if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) { + fprintf(stderr, "error: invalid window-size-chars: %s\n", optarg); + goto err; + } + + snprintf(buf, sizeof(buf), "initial-window-size-chars=%ux%u", width, height); + if (!push_string(&overrides, buf, &total_len)) + goto err; + break; + } + + case 'm': + if (!push_string(&overrides, "initial-window-mode=maximized", &total_len)) + goto err; + break; + + case 'F': + if (!push_string(&overrides, "initial-window-mode=fullscreen", &total_len)) + goto err; + break; + + case 's': + server_socket_path = optarg; + break; + + case 'H': + hold = true; + break; + + case 'N': + no_wait = true; + break; + + case 'o': + if (!push_string(&overrides, optarg, &total_len)) + goto err; + break; + + case 'E': + client_environment = true; + break; + + case 'd': { + int lvl = log_level_from_string(optarg); + if (unlikely(lvl < 0)) { + fprintf( + stderr, + "-d,--log-level: %s: argument must be one of %s\n", + optarg, + log_level_string_hint()); + goto err; + } + log_level = lvl; + break; + } + + case 'l': + if (optarg == NULL || streq(optarg, "auto")) + log_colorize = LOG_COLORIZE_AUTO; + else if (streq(optarg, "never")) + log_colorize = LOG_COLORIZE_NEVER; + else if (streq(optarg, "always")) + log_colorize = LOG_COLORIZE_ALWAYS; + else { + fprintf(stderr, "%s: argument must be one of 'never', 'always' or 'auto'\n", optarg); + goto err; + } + break; + + case 'v': + print_version_and_features("footclient "); + ret = EXIT_SUCCESS; + goto err; + + case 'h': + print_usage(prog_name); + ret = EXIT_SUCCESS; + goto err; + + case 'e': + break; + + case '?': + goto err; + } + } + + if (argc > 0) { + argc -= optind; + argv += optind; + } + + log_init(log_colorize, false, LOG_FACILITY_USER, log_level); + + fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd == -1) { + LOG_ERRNO("failed to create socket"); + goto err; + } + + struct sockaddr_un addr = {.sun_family = AF_UNIX}; + + if (server_socket_path != NULL) { + strncpy(addr.sun_path, server_socket_path, sizeof(addr.sun_path) - 1); + if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { + LOG_ERR("%s: failed to connect (is 'foot --server' running?)", server_socket_path); + goto err; + } + } else { + bool connected = false; + + const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); + if (xdg_runtime != NULL) { + const char *wayland_display = getenv("WAYLAND_DISPLAY"); + if (wayland_display != NULL) { + snprintf(addr.sun_path, sizeof(addr.sun_path), + "%s/foot-%s.sock", xdg_runtime, wayland_display); + connected = (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) == 0); + } + if (!connected) { + LOG_WARN("%s: failed to connect, will now try %s/foot.sock", + addr.sun_path, xdg_runtime); + snprintf(addr.sun_path, sizeof(addr.sun_path), + "%s/foot.sock", xdg_runtime); + connected = (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) == 0); + } + if (!connected) + LOG_WARN("%s: failed to connect, will now try /tmp/foot.sock", addr.sun_path); + } + + if (!connected) { + strncpy(addr.sun_path, "/tmp/foot.sock", sizeof(addr.sun_path) - 1); + if (connect(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { + LOG_ERRNO("failed to connect (is 'foot --server' running?)"); + goto err; + } + } + } + + const char *cwd = custom_cwd; + if (cwd == NULL) { + size_t buf_len = 1024; + do { + _cwd = xrealloc(_cwd, buf_len); + errno = 0; + if (getcwd(_cwd, buf_len) == NULL && errno != ERANGE) { + LOG_ERRNO("failed to get current working directory"); + goto err; + } + buf_len *= 2; + } while (errno == ERANGE); + cwd = _cwd; + } + + const char *pwd = getenv("PWD"); + if (pwd != NULL) { + char *resolved_path_cwd = realpath(cwd, NULL); + char *resolved_path_pwd = realpath(pwd, NULL); + + if (resolved_path_cwd != NULL && + resolved_path_pwd != NULL && + streq(resolved_path_cwd, resolved_path_pwd)) + { + /* + * The resolved path of $PWD matches the resolved path of + * the *actual* working directory - use $PWD. + * + * This makes a difference when $PWD refers to a symlink. + */ + cwd = pwd; + } + + free(resolved_path_cwd); + free(resolved_path_pwd); + } + + if (client_environment) { + for (char **e = environ; *e != NULL; e++) { + if (!push_string(&envp, *e, &total_len)) + goto err; + } + } + + /* String lengths, including NULL terminator */ + const size_t cwd_len = strlen(cwd) + 1; + const size_t override_count = tll_length(overrides); + const size_t env_count = tll_length(envp); + + const struct client_data data = { + .hold = hold, + .no_wait = no_wait, + .xdga_token = xdga_token, + .token_len = token_len, + .cwd_len = cwd_len, + .override_count = override_count, + .argc = argc, + .env_count = env_count, + }; + + /* Total packet length, not (yet) including argv[] */ + total_len += sizeof(data) + cwd_len + token_len; + + /* Add argv[] size to total packet length */ + cargv = xmalloc(argc * sizeof(cargv[0])); + for (size_t i = 0; i < argc; i++) { + const size_t arg_len = strlen(argv[i]) + 1; + + if (arg_len >= 1 << (8 * sizeof(cargv[i].len))) { + LOG_ERR("argv length overflow"); + goto err; + } + + cargv[i].len = arg_len; + total_len += sizeof(cargv[i]) + cargv[i].len; + } + + /* Check for size overflows */ + if (total_len >= 1llu << (8 * sizeof(uint32_t)) || + cwd_len >= 1 << (8 * sizeof(data.cwd_len)) || + token_len >= 1 << (8 * sizeof(data.token_len)) || + override_count > (size_t)(unsigned int)data.override_count || + argc > (int)(unsigned int)data.argc || + env_count > (size_t)(unsigned int)data.env_count) + { + LOG_ERR("size overflow"); + goto err; + } + + /* Send everything except argv[] */ + if (sendall(fd, &(uint32_t){total_len}, sizeof(uint32_t)) < 0 || + sendall(fd, &data, sizeof(data)) < 0 || + sendall(fd, cwd, cwd_len) < 0) + { + LOG_ERRNO("failed to send setup packet to server"); + goto err; + } + + /* Send XDGA token, if we have one */ + if (xdga_token) { + if (sendall(fd, token, token_len) != token_len) + { + LOG_ERRNO("failed to send xdg activation token to server"); + goto err; + } + } + + /* Send overrides */ + if (!send_string_list(fd, &overrides)) + goto err; + + /* Send argv[] */ + for (size_t i = 0; i < argc; i++) { + if (sendall(fd, &cargv[i], sizeof(cargv[i])) < 0 || + sendall(fd, argv[i], cargv[i].len) < 0) + { + LOG_ERRNO("failed to send setup packet (argv) to server"); + goto err; + } + } + + /* Send environment */ + if (!send_string_list(fd, &envp)) + goto err; + + struct sigaction sa_int = {.sa_handler = &sigint_handler}; + struct sigaction sa_usr = {.sa_handler = &sigusr_handler}; + sigemptyset(&sa_int.sa_mask); + sigemptyset(&sa_usr.sa_mask); + + if (sigaction(SIGINT, &sa_int, NULL) < 0 || + sigaction(SIGTERM, &sa_int, NULL) < 0 || + sigaction(SIGUSR1, &sa_usr, NULL) < 0 || + sigaction(SIGUSR2, &sa_usr, NULL) < 0) + { + LOG_ERRNO("failed to register signal handlers"); + goto err; + } + + int exit_code; + ssize_t rcvd = -1; + + while (true) { + rcvd = recv(fd, &exit_code, sizeof(exit_code), 0); + + const int got_sigusr = sigusr; + sigusr = 0; + + if (rcvd < 0 && errno == EINTR) { + if (aborted) + break; + else if (got_sigusr != 0) { + LOG_DBG("sending sigusr %d to server", got_sigusr); + + struct { + struct client_ipc_hdr hdr; + struct client_ipc_sigusr sigusr; + } ipc = { + .hdr = { + .ipc_code = FOOT_IPC_SIGUSR, + .size = sizeof(struct client_ipc_sigusr), + }, + .sigusr = { + .signo = got_sigusr, + }, + }; + + ssize_t count = send(fd, &ipc, sizeof(ipc), 0); + if (count < 0) { + LOG_ERRNO("failed to send SIGUSR IPC to server"); + goto err; + } else if ((size_t)count != sizeof(ipc)) { + LOG_ERR("failed to send SIGUSR IPC to server"); + goto err; + } + } + + continue; + } + + break; + } + + if (rcvd == -1 && errno == EINTR) + xassert(aborted); + else if (rcvd != sizeof(exit_code)) + LOG_ERRNO("failed to read server response"); + else + ret = exit_code; + +err: + free_string_list(&envp); + free_string_list(&overrides); + free(cargv); + free(_cwd); + if (fd != -1) + close(fd); + log_deinit(); + return ret; +} diff --git a/commands.c b/commands.c new file mode 100644 index 0000000..a3e4845 --- /dev/null +++ b/commands.c @@ -0,0 +1,115 @@ +#include "commands.h" + +#define LOG_MODULE "commands" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "grid.h" +#include "render.h" +#include "selection.h" +#include "terminal.h" +#include "url-mode.h" +#include "util.h" + +void +cmd_scrollback_up(struct terminal *term, int rows) +{ + if (term->grid == &term->alt) + return; + if (urls_mode_is_active(term)) + return; + + const struct grid *grid = term->grid; + const int view = grid->view; + const int grid_rows = grid->num_rows; + + /* The view row number in scrollback relative coordinates. This is + * the maximum number of rows we're allowed to scroll */ + int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); + int view_sb_rel = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, view); + + rows = min(rows, view_sb_rel); + if (rows == 0) + return; + + int new_view = (view + grid_rows) - rows; + new_view &= grid_rows - 1; + + xassert(new_view != view); + xassert(grid->rows[new_view] != NULL); +#if defined(_DEBUG) + for (int r = 0; r < term->rows; r++) + xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL); +#endif + + LOG_DBG("scrollback UP: %d -> %d (offset = %d, rows = %d)", + view, new_view, offset, grid_rows); + + selection_view_up(term, new_view); + term->grid->view = new_view; + + if (rows < term->rows) { + term_damage_scroll( + term, DAMAGE_SCROLL_REVERSE_IN_VIEW, + (struct scroll_region){0, term->rows}, rows); + term_damage_rows_in_view(term, 0, rows - 1); + } else + term_damage_view(term); + + render_refresh_urls(term); + render_refresh(term); +} + +void +cmd_scrollback_down(struct terminal *term, int rows) +{ + if (term->grid == &term->alt) + return; + if (urls_mode_is_active(term)) + return; + + const struct grid *grid = term->grid; + const int offset = grid->offset; + const int view = grid->view; + const int grid_rows = grid->num_rows; + const int screen_rows = term->rows; + + const int scrollback_end = offset; + + /* Number of rows to scroll, without going past the scrollback end */ + int max_rows = 0; + if (view <= scrollback_end) + max_rows = scrollback_end - view; + else + max_rows = offset + (grid_rows - view); + + rows = min(rows, max_rows); + if (rows == 0) + return; + + int new_view = (view + rows) & (grid_rows - 1); + + xassert(new_view != view); + xassert(grid->rows[new_view] != NULL); +#if defined(_DEBUG) + for (int r = 0; r < term->rows; r++) + xassert(grid->rows[(new_view + r) & (grid_rows - 1)] != NULL); +#endif + + LOG_DBG("scrollback DOWN: %d -> %d (offset = %d, rows = %d)", + view, new_view, offset, grid_rows); + + selection_view_down(term, new_view); + term->grid->view = new_view; + + if (rows < term->rows) { + term_damage_scroll( + term, DAMAGE_SCROLL_IN_VIEW, + (struct scroll_region){0, term->rows}, rows); + term_damage_rows_in_view(term, term->rows - rows, screen_rows - 1); + } else + term_damage_view(term); + + render_refresh_urls(term); + render_refresh(term); +} diff --git a/commands.h b/commands.h new file mode 100644 index 0000000..644523b --- /dev/null +++ b/commands.h @@ -0,0 +1,6 @@ +#pragma once + +#include "terminal.h" + +void cmd_scrollback_up(struct terminal *term, int rows); +void cmd_scrollback_down(struct terminal *term, int rows); diff --git a/completions/bash/foot b/completions/bash/foot new file mode 100644 index 0000000..e27be2f --- /dev/null +++ b/completions/bash/foot @@ -0,0 +1,90 @@ +# Bash completion script for foot +_foot() +{ + COMPREPLY=() + + local cur prev flags word commands match previous_words i offset + flags=( + "--app-id" + "--toplevel-tag" + "--check-config" + "--config" + "--font" + "--fullscreen" + "--help" + "--hold" + "--log-colorize" + "--log-level" + "--log-no-syslog" + "--login-shell" + "--maximized" + "--override" + "--print-pid" + "--pty" + "--server" + "--term" + "--title" + "--version" + "--window-size-pixels" + "--window-size-chars" + "--working-directory" + ) + flags="${flags[@]}" + cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD-1]} + + # Check if positional argument is completed + previous_words=( "${COMP_WORDS[@]}" ) + unset previous_words[-1] + commands=$(compgen -c | grep -vFx "$(compgen -k)" | grep -vE '^([.:[]|foot)$' | sort -u) + i=0 + for word in "${previous_words[@]}" ; do + match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null) + if [[ ! -z "$match" ]] ; then + if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--toplevel-tag|--config|--font|--log-level|--pty|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then + (( i++ )) + continue + fi + # Positional argument found + offset=$i + fi + (( i++ )) + done + + if [[ ! -z "$offset" ]] ; then + # Depends on bash_completion being available + declare -F _command_offset >/dev/null || return 1 + _command_offset $offset + return 0 + elif [[ ${cur} == --* ]] ; then + COMPREPLY=( $(compgen -W "${flags}" -- ${cur}) ) + return 0 + fi + + case "$prev" in + --config|--print-pid|--server|-[cps]) + compopt -o default ;; + --working-directory|-D) + compopt -o dirnames ;; + --term|-t) + command -v toe > /dev/null || return 1 + COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 !~ /[+]/ {print $1}')" -- ${cur}) ) ;; + --font|-f) + command -v fc-list > /dev/null || return 1 + COMPREPLY=( $(compgen -W "$(fc-list : family | sed 's/,/\n/g' | uniq | tr -d ' ')" -- ${cur}) ) ;; + --log-level|-d) + COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; + --log-colorize|-l) + COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; + --app-id|--toplevel-tag|--help|--override|--pty|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC]) + # Don't autocomplete for these flags + : ;; + *) + # Complete commands from $PATH + COMPREPLY=( $(compgen -c -- ${cur}) ) ;; + esac + + return 0 +} + +complete -F _foot foot diff --git a/completions/bash/footclient b/completions/bash/footclient new file mode 100644 index 0000000..c7f1df4 --- /dev/null +++ b/completions/bash/footclient @@ -0,0 +1,82 @@ +# Bash completion script for footclient +_footclient() +{ + COMPREPLY=() + + local cur prev flags word commands match previous_words i offset + flags=( + "--app-id" + "--toplevel-tag" + "--fullscreen" + "--help" + "--hold" + "--login-shell" + "--log-level" + "--log-colorize" + "--maximized" + "--override" + "--client-environment" + "--server-socket" + "--term" + "--title" + "--version" + "--window-size-pixels" + "--window-size-chars" + "--working-directory" + ) + flags="${flags[@]}" + cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD-1]} + + # Check if positional argument is completed + previous_words=( "${COMP_WORDS[@]}" ) + unset previous_words[-1] + commands=$(compgen -c | grep -vFx "$(compgen -k)" | grep -vE '^([.:[]|footclient)$' | sort -u) + i=0 + for word in "${previous_words[@]}" ; do + match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null) + if [[ ! -z "$match" ]] ; then + if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--toplevel-tag|--log-level|--server-socket|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then + (( i++ )) + continue + fi + # Positional argument found + offset=$i + fi + (( i++ )) + done + + if [[ ! -z "$offset" ]] ; then + # Depends on bash_completion being available + declare -F _command_offset >/dev/null || return 1 + _command_offset $offset + return 0 + elif [[ ${cur} == --* ]] ; then + COMPREPLY=( $(compgen -W "${flags}" -- ${cur}) ) + return 0 + fi + + case "$prev" in + --server-socket|-s) + compopt -o default ;; + --working-directory|-D) + compopt -o dirnames ;; + --term|-t) + command -v toe > /dev/null || return 1 + COMPREPLY=( $(compgen -W "$(toe -a | awk '$1 ~ /[+]/ {next}; {print $1}')" -- ${cur}) ) ;; + --log-level|-d) + COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; + --log-colorize|-l) + COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; + --app-id|--toplevel-tag|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|-[ahoTvWw]) + # Don't autocomplete for these flags + : ;; + *) + # Complete commands from $PATH + COMPREPLY=( $(compgen -c -- ${cur}) ) ;; + esac + + return 0 +} + +complete -F _footclient footclient diff --git a/completions/fish/foot.fish b/completions/fish/foot.fish new file mode 100644 index 0000000..21b42d3 --- /dev/null +++ b/completions/fish/foot.fish @@ -0,0 +1,24 @@ +complete -c foot -x -a "(__fish_complete_subcommand)" +complete -c foot -r -s c -l config -d "path to configuration file (XDG_CONFIG_HOME/foot/foot.ini)" +complete -c foot -s C -l check-config -d "verify configuration and exit with 0 if ok, otherwise exit with 1" +complete -c foot -x -s o -l override -d "configuration option to override, in form SECTION.KEY=VALUE" +complete -c foot -x -s f -l font -a "(fc-list : family | sed 's/,/\n/g' | sort | uniq)" -d "font name and style in fontconfig format (monospace)" +complete -c foot -x -s t -l term -a '(find /usr/share/terminfo -type f -printf "%f\n")' -d "value to set the environment variable TERM to (foot)" +complete -c foot -x -s T -l title -d "initial window title" +complete -c foot -x -s a -l app-id -d "value to set the app-id property on the Wayland window to (foot)" +complete -c foot -x -l toplevel-tag -d "value to set the toplevel-tag property on the Wayland window to" +complete -c foot -s m -l maximized -d "start in maximized mode" +complete -c foot -s F -l fullscreen -d "start in fullscreen mode" +complete -c foot -s L -l login-shell -d "start shell as a login shell" +complete -c foot -F -s D -l working-directory -d "initial working directory for the client application (CWD)" +complete -c foot -x -s w -l window-size-pixels -d "window WIDTHxHEIGHT, in pixels (700x500)" +complete -c foot -x -s W -l window-size-chars -d "window WIDTHxHEIGHT, in characters (not set)" +complete -c foot -F -s s -l server -d "run as server; open terminals by running footclient" +complete -c foot -s H -l hold -d "remain open after child process exits" +complete -c foot -r -s p -l print-pid -d "print PID to this file or FD when up and running (server mode only)" +complete -c foot -x -s d -l log-level -a "info warning error none" -d "log-level (warning)" +complete -c foot -x -s l -l log-colorize -a "always never auto" -d "enable or disable colorization of log output on stderr" +complete -c foot -s S -l log-no-syslog -d "disable syslog logging (server mode only)" +complete -c foot -r -l pty -d "display an existing pty instead of creating one" +complete -c foot -s v -l version -d "show the version number and quit" +complete -c foot -s h -l help -d "show help message and quit" diff --git a/completions/fish/footclient.fish b/completions/fish/footclient.fish new file mode 100644 index 0000000..0362479 --- /dev/null +++ b/completions/fish/footclient.fish @@ -0,0 +1,20 @@ +complete -c footclient -x -a "(__fish_complete_subcommand)" +complete -c footclient -x -s t -l term -a '(find /usr/share/terminfo -type f -printf "%f\n")' -d "value to set the environment variable TERM to (foot)" +complete -c footclient -x -s T -l title -d "initial window title" +complete -c footclient -x -s a -l app-id -d "value to set the app-id property on the Wayland window to (foot)" +complete -c footclient -x -l toplevel-tag -d "value to set the toplevel-tag property on the Wayland window to" +complete -c footclient -s m -l maximized -d "start in maximized mode" +complete -c footclient -s F -l fullscreen -d "start in fullscreen mode" +complete -c footclient -s L -l login-shell -d "start shell as a login shell" +complete -c footclient -F -s D -l working-directory -d "initial working directory for the client application (CWD)" +complete -c footclient -x -s w -l window-size-pixels -d "window WIDTHxHEIGHT, in pixels (700x500)" +complete -c footclient -x -s W -l window-size-chars -d "window WIDTHxHEIGHT, in characters (not set)" +complete -c footclient -F -s s -l server-socket -d "override the default path to the foot server socket ($XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock)" +complete -c footclient -s H -l hold -d "remain open after child process exits" +complete -c footclient -s N -l no-wait -d "detach the client process from the running terminal, exiting immediately" +complete -c footclient -x -s o -l override -d "configuration option to override, in form SECTION.KEY=VALUE" +complete -c footclient -s E -l client-environment -d "child process inherits footclient's environment, instead of the server's" +complete -c footclient -x -s d -l log-level -a "info warning error none" -d "log-level (info)" +complete -c footclient -x -s l -l log-colorize -a "always never auto" -d "enable or disable colorization of log output on stderr" +complete -c footclient -s v -l version -d "show the version number and quit" +complete -c footclient -s h -l help -d "show help message and quit" diff --git a/completions/meson.build b/completions/meson.build new file mode 100644 index 0000000..b0da945 --- /dev/null +++ b/completions/meson.build @@ -0,0 +1,9 @@ +zsh_install_dir = join_paths(get_option('datadir'), 'zsh', 'site-functions') +fish_install_dir = join_paths(get_option('datadir'), 'fish', 'vendor_completions.d') +bash_install_dir = join_paths(get_option('datadir'), 'bash-completion', 'completions') +install_data('zsh/_foot', install_dir: zsh_install_dir) +install_data('zsh/_footclient', install_dir: zsh_install_dir) +install_data('fish/foot.fish', install_dir: fish_install_dir) +install_data('fish/footclient.fish', install_dir: fish_install_dir) +install_data('bash/foot', install_dir: bash_install_dir) +install_data('bash/footclient', install_dir: bash_install_dir) diff --git a/completions/zsh/_foot b/completions/zsh/_foot new file mode 100644 index 0000000..0fd83b3 --- /dev/null +++ b/completions/zsh/_foot @@ -0,0 +1,41 @@ +#compdef foot + +_arguments \ + -s -S -C \ + '(-c --config)'{-c,--config}'[path to configuration file (XDG_CONFIG_HOME/foot/foot.ini)]:config:_files' \ + '(-C --check-config)'{-C,--check-config}'[verify configuration and exit with 0 if ok, otherwise exit with 1]' \ + '(-o --override)'{-o,--override}'[configuration option to override, in form SECTION.KEY=VALUE]:()' \ + '(-f --font)'{-f,--font}'[font name and style in fontconfig format (monospace)]:font:->fonts' \ + '(-t --term)'{-t,--term}'[value to set the environment variable TERM to (foot)]:term:->terms' \ + '(-T --title)'{-T,--title}'[initial window title]:()' \ + '(-a --app-id)'{-a,--app-id}'[value to set the app-id property on the Wayland window to (foot)]:()' \ + '--toplevel-tag=[value to set the toplevel-tag property on the Wayland window to]:()' \ + '(-m --maximized)'{-m,--maximized}'[start in maximized mode]' \ + '(-F --fullscreen)'{-F,--fullscreen}'[start in fullscreen mode]' \ + '(-L --login-shell)'{-L,--login-shell}'[start shell as a login shell]' \ + '(-D --working-directory)'{-D,--working-directory}'[initial working directory for the client application (CWD)]:working_directory:_files' \ + '(-w --window-size-pixels)'{-w,--window-size-pixels}'[window WIDTHxHEIGHT, in pixels (700x500)]:size_pixels:()' \ + '(-W --window-size-chars)'{-W,--window-size-chars}'[window WIDTHxHEIGHT, in characters (not set)]:size_chars:()' \ + '(-s --server)'{-s,--server}'[run as server; open terminals by running footclient]:server:_files' \ + '(-H --hold)'{-H,--hold}'[remain open after child process exits]' \ + '(-p --print-pid)'{-p,--print-pid}'[print PID to this file or FD when up and running (server mode only)]:pidfile:_files' \ + '--pty=[display an existing pty instead of creating one]:pty:_files' \ + '(-d --log-level)'{-d,--log-level}'[log level (warning)]:loglevel:(info warning error none)' \ + '(-l --log-colorize)'{-l,--log-colorize}'[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \ + '(-S --log-no-syslog)'{-s,--log-no-syslog}'[disable syslog logging (server mode only)]' \ + '(-v --version)'{-v,--version}'[show the version number and quit]' \ + '(-h --help)'{-h,--help}'[show help message and quit]' \ + ':command: _command_names -e' \ + '*::command arguments: _dispatch ${words[1]} ${words[1]}' + +case ${state} in + fonts) + IFS=$'\n' + _values -s , 'font families' $(fc-list : family | sed 's/,/\n/g' | sort | uniq) + unset IFS + ;; + + terms) + _values 'terminal definitions' /usr/share/terminfo/**/*(.:t) + ;; +esac diff --git a/completions/zsh/_footclient b/completions/zsh/_footclient new file mode 100644 index 0000000..12f29d7 --- /dev/null +++ b/completions/zsh/_footclient @@ -0,0 +1,31 @@ +#compdef footclient + +_arguments \ + -s -S -C \ + '(-t --term)'{-t,--term}'[value to set the environment variable TERM to (foot)]:term:->terms' \ + '(-T --title)'{-T,--title}'[initial window title]:()' \ + '(-a --app-id)'{-a,--app-id}'[value to set the app-id property on the Wayland window to (foot)]:()' \ + '--toplevel-tag=[value to set the toplevel-tag property on the Wayland window to]:()' \ + '(-m --maximized)'{-m,--maximized}'[start in maximized mode]' \ + '(-F --fullscreen)'{-F,--fullscreen}'[start in fullscreen mode]' \ + '(-L --login-shell)'{-L,--login-shell}'[start shell as a login shell]' \ + '(-D --working-directory)'{-D,--working-directory}'[initial working directory for the client application (CWD)]:working_directory:_files' \ + '(-w --window-size-pixels)'{-w,--window-size-pixels}'[window WIDTHxHEIGHT, in pixels (700x500)]:size_pixels:()' \ + '(-W --window-size-chars)'{-W,--window-size-chars}'[window WIDTHxHEIGHT, in characters (not set)]:size_chars:()' \ + '(-s --server-socket)'{-s,--server-socket}'[override the default path to the foot server socket ($XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock)]:server:_files' \ + '(-H --hold)'{-H,--hold}'[remain open after child process exits]' \ + '(-N --no-wait)'{-N,--no-wait}'[detach the client process from the running terminal, exiting immediately]' \ + '(-o --override)'{-o,--override}'[configuration option to override, in form SECTION.KEY=VALUE]:()' \ + '(-E --client-environment)'{-E,--client-environment}"[child process inherits footclient's environment, instead of the server's]" \ + '(-d --log-level)'{-d,--log-level}'[log level (warning)]:loglevel:(info warning error none)' \ + '(-l --log-colorize)'{-l,--log-colorize}'[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \ + '(-v --version)'{-v,--version}'[show the version number and quit]' \ + '(-h --help)'{-h,--help}'[show help message and quit]' \ + ':command: _command_names -e' \ + '*::command arguments: _dispatch ${words[1]} ${words[1]}' + +case ${state} in + terms) + _values 'terminal definitions' /usr/share/terminfo/**/*(.:t) + ;; +esac diff --git a/composed.c b/composed.c new file mode 100644 index 0000000..fc7dfa0 --- /dev/null +++ b/composed.c @@ -0,0 +1,149 @@ +#include "composed.h" + +#include +#include + +#include "debug.h" +#include "terminal.h" + +uint32_t +composed_key_from_chars(const uint32_t chars[], size_t count) +{ + if (count == 0) + return 0; + + uint32_t key = chars[0]; + for (size_t i = 1; i < count; i++) + key = composed_key_from_key(key, chars[i]); + + return key; +} + +uint32_t +composed_key_from_key(uint32_t prev_key, uint32_t next_char) +{ + unsigned bits = 32 - __builtin_clz(CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO); + + /* Rotate old key 8 bits */ + uint32_t new_key = (prev_key << 8) | (prev_key >> (bits - 8)); + + /* xor with new char */ + new_key ^= next_char; + + /* Multiply with magic hash constant */ + new_key *= 2654435761ul; + + /* And mask, to ensure the new value is within range */ + new_key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; + return new_key; +} + +UNITTEST +{ + const char32_t chars[] = U"abcdef"; + + uint32_t k1 = composed_key_from_key(chars[0], chars[1]); + uint32_t k2 = composed_key_from_chars(chars, 2); + xassert(k1 == k2); + + uint32_t k3 = composed_key_from_key(k2, chars[2]); + uint32_t k4 = composed_key_from_chars(chars, 3); + xassert(k3 == k4); +} + +const struct composed * +composed_lookup(struct composed *root, uint32_t key) +{ + struct composed *node = root; + + while (node != NULL) { + if (key == node->key) + return node; + + node = key < node->key ? node->left : node->right; + } + + return NULL; +} + +const struct composed * +composed_lookup_without_collision(struct composed *root, uint32_t *key, + const char32_t *prefix_text, size_t prefix_len, + char32_t wc, int forced_width) +{ + while (true) { + const struct composed *cc = composed_lookup(root, *key); + if (cc == NULL) + return NULL; + + bool match = cc->count == prefix_len + 1 && + cc->forced_width == forced_width && + cc->chars[prefix_len] == wc; + + if (match) { + for (size_t i = 0; i < prefix_len; i++) { + if (cc->chars[i] != prefix_text[i]) { + match = false; + break; + } + } + } + + if (match) + return cc; + + (*key)++; + *key &= CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO; + + /* TODO: this will loop infinitely if the composed table is full */ + } + + return NULL; +} + +void +composed_insert(struct composed **root, struct composed *node) +{ + node->left = node->right = NULL; + + if (*root == NULL) { + *root = node; + return; + } + + uint32_t key = node->key; + + struct composed *prev = NULL; + struct composed *n = *root; + + while (n != NULL) { + xassert(n->key != node->key); + + prev = n; + n = key < n->key ? n->left : n->right; + } + + xassert(prev != NULL); + xassert(n == NULL); + + if (key < prev->key) { + xassert(prev->left == NULL); + prev->left = node; + } else { + xassert(prev->right == NULL); + prev->right = node; + } +} + +void +composed_free(struct composed *root) +{ + if (root == NULL) + return; + + composed_free(root->left); + composed_free(root->right); + + free(root->chars); + free(root); +} diff --git a/composed.h b/composed.h new file mode 100644 index 0000000..18afb14 --- /dev/null +++ b/composed.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +struct composed { + char32_t *chars; + struct composed *left; + struct composed *right; + uint32_t key; + uint8_t count; + uint8_t width; + uint8_t forced_width; +}; + +uint32_t composed_key_from_chars(const uint32_t chars[], size_t count); +uint32_t composed_key_from_key(uint32_t prev_key, uint32_t next_char); + +const struct composed *composed_lookup(struct composed *root, uint32_t key); +const struct composed *composed_lookup_without_collision( + struct composed *root, uint32_t *key, + const char32_t *prefix, size_t prefix_len, char32_t wc, int forced_width); +void composed_insert(struct composed **root, struct composed *node); + +void composed_free(struct composed *root); diff --git a/config.c b/config.c new file mode 100644 index 0000000..c02f5b7 --- /dev/null +++ b/config.c @@ -0,0 +1,4366 @@ +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#define LOG_MODULE "config" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "char32.h" +#include "debug.h" +#include "input.h" +#include "key-binding.h" +#include "macros.h" +#include "tokenize.h" +#include "util.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +static const uint32_t default_foreground = 0xffffff; +static const uint32_t default_background = 0x242424; + +static const size_t min_csd_border_width = 5; + +#define cube6(r, g) \ + r|g|0x00, r|g|0x5f, r|g|0x87, r|g|0xaf, r|g|0xd7, r|g|0xff + +#define cube36(r) \ + cube6(r, 0x0000), \ + cube6(r, 0x5f00), \ + cube6(r, 0x8700), \ + cube6(r, 0xaf00), \ + cube6(r, 0xd700), \ + cube6(r, 0xff00) + +static const uint32_t default_color_table[256] = { + // Regular + 0x242424, + 0xf62b5a, + 0x47b413, + 0xe3c401, + 0x24acd4, + 0xf2affd, + 0x13c299, + 0xe6e6e6, + + // Bright + 0x616161, + 0xff4d51, + 0x35d450, + 0xe9e836, + 0x5dc5f8, + 0xfeabf2, + 0x24dfc4, + 0xffffff, + + // 6x6x6 RGB cube + // (color channels = i ? i*40+55 : 0, where i = 0..5) + cube36(0x000000), + cube36(0x5f0000), + cube36(0x870000), + cube36(0xaf0000), + cube36(0xd70000), + cube36(0xff0000), + + // 24 shades of gray + // (color channels = i*10+8, where i = 0..23) + 0x080808, 0x121212, 0x1c1c1c, 0x262626, + 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e, + 0x585858, 0x626262, 0x6c6c6c, 0x767676, + 0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e, + 0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, + 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee +}; + +/* VT330/VT340 Programmer Reference Manual - Table 2-3 VT340 Default Color Map */ +static const uint32_t default_sixel_colors[16] = { + 0xff000000, + 0xff3333cc, + 0xffcc2121, + 0xff33cc33, + 0xffcc33cc, + 0xff33cccc, + 0xffcccc33, + 0xff878787, + 0xff424242, + 0xff545499, + 0xff994242, + 0xff549954, + 0xff995499, + 0xff549999, + 0xff999954, + 0xffcccccc, +}; + +static const char *const binding_action_map[] = { + [BIND_ACTION_NONE] = NULL, + [BIND_ACTION_NOOP] = "noop", + [BIND_ACTION_SCROLLBACK_UP_PAGE] = "scrollback-up-page", + [BIND_ACTION_SCROLLBACK_UP_HALF_PAGE] = "scrollback-up-half-page", + [BIND_ACTION_SCROLLBACK_UP_LINE] = "scrollback-up-line", + [BIND_ACTION_SCROLLBACK_DOWN_PAGE] = "scrollback-down-page", + [BIND_ACTION_SCROLLBACK_DOWN_HALF_PAGE] = "scrollback-down-half-page", + [BIND_ACTION_SCROLLBACK_DOWN_LINE] = "scrollback-down-line", + [BIND_ACTION_SCROLLBACK_HOME] = "scrollback-home", + [BIND_ACTION_SCROLLBACK_END] = "scrollback-end", + [BIND_ACTION_CLIPBOARD_COPY] = "clipboard-copy", + [BIND_ACTION_CLIPBOARD_PASTE] = "clipboard-paste", + [BIND_ACTION_PRIMARY_PASTE] = "primary-paste", + [BIND_ACTION_SEARCH_START] = "search-start", + [BIND_ACTION_FONT_SIZE_UP] = "font-increase", + [BIND_ACTION_FONT_SIZE_DOWN] = "font-decrease", + [BIND_ACTION_FONT_SIZE_RESET] = "font-reset", + [BIND_ACTION_SPAWN_TERMINAL] = "spawn-terminal", + [BIND_ACTION_MINIMIZE] = "minimize", + [BIND_ACTION_MAXIMIZE] = "maximize", + [BIND_ACTION_FULLSCREEN] = "fullscreen", + [BIND_ACTION_PIPE_SCROLLBACK] = "pipe-scrollback", + [BIND_ACTION_PIPE_VIEW] = "pipe-visible", + [BIND_ACTION_PIPE_SELECTED] = "pipe-selected", + [BIND_ACTION_PIPE_COMMAND_OUTPUT] = "pipe-command-output", + [BIND_ACTION_SHOW_URLS_COPY] = "show-urls-copy", + [BIND_ACTION_SHOW_URLS_LAUNCH] = "show-urls-launch", + [BIND_ACTION_SHOW_URLS_PERSISTENT] = "show-urls-persistent", + [BIND_ACTION_TEXT_BINDING] = "text-binding", + [BIND_ACTION_PROMPT_PREV] = "prompt-prev", + [BIND_ACTION_PROMPT_NEXT] = "prompt-next", + [BIND_ACTION_UNICODE_INPUT] = "unicode-input", + [BIND_ACTION_QUIT] = "quit", + [BIND_ACTION_REGEX_LAUNCH] = "regex-launch", + [BIND_ACTION_REGEX_COPY] = "regex-copy", + [BIND_ACTION_THEME_SWITCH_1] = "color-theme-switch-1", + [BIND_ACTION_THEME_SWITCH_2] = "color-theme-switch-2", + [BIND_ACTION_THEME_SWITCH_DARK] = "color-theme-switch-dark", + [BIND_ACTION_THEME_SWITCH_LIGHT] = "color-theme-switch-light", + [BIND_ACTION_THEME_TOGGLE] = "color-theme-toggle", + + /* Tab actions */ + [BIND_ACTION_TAB_NEW] = "tab-new", + [BIND_ACTION_TAB_CLOSE] = "tab-close", + [BIND_ACTION_TAB_NEXT] = "tab-next", + [BIND_ACTION_TAB_PREV] = "tab-prev", + [BIND_ACTION_TAB_1] = "tab-1", + [BIND_ACTION_TAB_2] = "tab-2", + [BIND_ACTION_TAB_3] = "tab-3", + [BIND_ACTION_TAB_4] = "tab-4", + [BIND_ACTION_TAB_5] = "tab-5", + [BIND_ACTION_TAB_6] = "tab-6", + [BIND_ACTION_TAB_7] = "tab-7", + [BIND_ACTION_TAB_8] = "tab-8", + [BIND_ACTION_TAB_9] = "tab-9", + [BIND_ACTION_TAB_OVERVIEW] = "tab-overview", + + /* Mouse-specific actions */ + [BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse", + [BIND_ACTION_SCROLLBACK_DOWN_MOUSE] = "scrollback-down-mouse", + [BIND_ACTION_SELECT_BEGIN] = "select-begin", + [BIND_ACTION_SELECT_BEGIN_BLOCK] = "select-begin-block", + [BIND_ACTION_SELECT_EXTEND] = "select-extend", + [BIND_ACTION_SELECT_EXTEND_CHAR_WISE] = "select-extend-character-wise", + [BIND_ACTION_SELECT_WORD] = "select-word", + [BIND_ACTION_SELECT_WORD_WS] = "select-word-whitespace", + [BIND_ACTION_SELECT_QUOTE] = "select-quote", + [BIND_ACTION_SELECT_ROW] = "select-row", +}; + +static const char *const search_binding_action_map[] = { + [BIND_ACTION_SEARCH_NONE] = NULL, + [BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE] = "scrollback-up-page", + [BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE] = "scrollback-up-half-page", + [BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE] = "scrollback-up-line", + [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE] = "scrollback-down-page", + [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE] = "scrollback-down-half-page", + [BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE] = "scrollback-down-line", + [BIND_ACTION_SEARCH_SCROLLBACK_HOME] = "scrollback-home", + [BIND_ACTION_SEARCH_SCROLLBACK_END] = "scrollback-end", + [BIND_ACTION_SEARCH_CANCEL] = "cancel", + [BIND_ACTION_SEARCH_COMMIT] = "commit", + [BIND_ACTION_SEARCH_FIND_PREV] = "find-prev", + [BIND_ACTION_SEARCH_FIND_NEXT] = "find-next", + [BIND_ACTION_SEARCH_EDIT_LEFT] = "cursor-left", + [BIND_ACTION_SEARCH_EDIT_LEFT_WORD] = "cursor-left-word", + [BIND_ACTION_SEARCH_EDIT_RIGHT] = "cursor-right", + [BIND_ACTION_SEARCH_EDIT_RIGHT_WORD] = "cursor-right-word", + [BIND_ACTION_SEARCH_EDIT_HOME] = "cursor-home", + [BIND_ACTION_SEARCH_EDIT_END] = "cursor-end", + [BIND_ACTION_SEARCH_DELETE_PREV] = "delete-prev", + [BIND_ACTION_SEARCH_DELETE_PREV_WORD] = "delete-prev-word", + [BIND_ACTION_SEARCH_DELETE_NEXT] = "delete-next", + [BIND_ACTION_SEARCH_DELETE_NEXT_WORD] = "delete-next-word", + [BIND_ACTION_SEARCH_DELETE_TO_START] = "delete-to-start", + [BIND_ACTION_SEARCH_DELETE_TO_END] = "delete-to-end", + [BIND_ACTION_SEARCH_EXTEND_CHAR] = "extend-char", + [BIND_ACTION_SEARCH_EXTEND_WORD] = "extend-to-word-boundary", + [BIND_ACTION_SEARCH_EXTEND_WORD_WS] = "extend-to-next-whitespace", + [BIND_ACTION_SEARCH_EXTEND_LINE_DOWN] = "extend-line-down", + [BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR] = "extend-backward-char", + [BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD] = "extend-backward-to-word-boundary", + [BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS] = "extend-backward-to-next-whitespace", + [BIND_ACTION_SEARCH_EXTEND_LINE_UP] = "extend-line-up", + [BIND_ACTION_SEARCH_CLIPBOARD_PASTE] = "clipboard-paste", + [BIND_ACTION_SEARCH_PRIMARY_PASTE] = "primary-paste", + [BIND_ACTION_SEARCH_UNICODE_INPUT] = "unicode-input", + [BIND_ACTION_SEARCH_TOGGLE_CASE] = "toggle-case", + [BIND_ACTION_SEARCH_TOGGLE_WHOLE_WORD] = "toggle-whole-word", + [BIND_ACTION_SEARCH_TOGGLE_REGEX] = "toggle-regex", + [BIND_ACTION_SEARCH_HISTORY_PREV] = "history-prev", + [BIND_ACTION_SEARCH_HISTORY_NEXT] = "history-next", + [BIND_ACTION_SEARCH_COMMIT_LINE] = "commit-line", +}; + +static const char *const url_binding_action_map[] = { + [BIND_ACTION_URL_NONE] = NULL, + [BIND_ACTION_URL_CANCEL] = "cancel", + [BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL] = "toggle-url-visible", +}; + +static_assert(ALEN(binding_action_map) == BIND_ACTION_COUNT, + "binding action map size mismatch"); +static_assert(ALEN(search_binding_action_map) == BIND_ACTION_SEARCH_COUNT, + "search binding action map size mismatch"); +static_assert(ALEN(url_binding_action_map) == BIND_ACTION_URL_COUNT, + "URL binding action map size mismatch"); + +struct context { + struct config *conf; + const char *section; + const char *section_suffix; + const char *key; + const char *value; + + const char *path; + unsigned lineno; + + bool errors_are_fatal; +}; + +static const enum user_notification_kind log_class_to_notify_kind[LOG_CLASS_COUNT] = { + [LOG_CLASS_WARNING] = USER_NOTIFICATION_WARNING, + [LOG_CLASS_ERROR] = USER_NOTIFICATION_ERROR, +}; + +static void NOINLINE VPRINTF(5) +log_and_notify_va(struct config *conf, enum log_class log_class, + const char *file, int lineno, const char *fmt, va_list va) +{ + xassert(log_class < ALEN(log_class_to_notify_kind)); + enum user_notification_kind kind = log_class_to_notify_kind[log_class]; + + if (kind == 0) { + BUG("unsupported log class: %d", (int)log_class); + return; + } + + char *formatted_msg = xvasprintf(fmt, va); + log_msg(log_class, LOG_MODULE, file, lineno, "%s", formatted_msg); + user_notification_add(&conf->notifications, kind, formatted_msg); +} + +static void NOINLINE PRINTF(5) +log_and_notify(struct config *conf, enum log_class log_class, + const char *file, int lineno, const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + log_and_notify_va(conf, log_class, file, lineno, fmt, va); + va_end(va); +} + +static void NOINLINE PRINTF(5) +log_contextual(struct context *ctx, enum log_class log_class, + const char *file, int lineno, const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + char *formatted_msg = xvasprintf(fmt, va); + va_end(va); + + const bool print_dot = ctx->key != NULL; + const bool print_colon = ctx->value != NULL; + const bool print_section_suffix = ctx->section_suffix != NULL; + + if (!print_dot) + ctx->key = ""; + + if (!print_colon) + ctx->value = ""; + + if (!print_section_suffix) + ctx->section_suffix = ""; + + log_and_notify( + ctx->conf, log_class, file, lineno, "%s:%d: [%s%s%s]%s%s%s%s: %s", + ctx->path, ctx->lineno, ctx->section, + print_section_suffix ? ":" : "", ctx->section_suffix, + print_dot ? "." : "", ctx->key, print_colon ? ": " : "", + ctx->value, formatted_msg); + free(formatted_msg); +} + + +static void NOINLINE VPRINTF(4) +log_and_notify_errno_va(struct config *conf, const char *file, int lineno, + const char *fmt, va_list va) +{ + int errno_copy = errno; + char *formatted_msg = xvasprintf(fmt, va); + log_and_notify( + conf, LOG_CLASS_ERROR, file, lineno, + "%s: %s", formatted_msg, strerror(errno_copy)); + free(formatted_msg); +} + +static void NOINLINE PRINTF(4) +log_and_notify_errno(struct config *conf, const char *file, int lineno, + const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + log_and_notify_errno_va(conf, file, lineno, fmt, va); + va_end(va); +} + +static void NOINLINE PRINTF(4) +log_contextual_errno(struct context *ctx, const char *file, int lineno, + const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + char *formatted_msg = xvasprintf(fmt, va); + va_end(va); + + bool print_dot = ctx->key != NULL; + bool print_colon = ctx->value != NULL; + + if (!print_dot) + ctx->key = ""; + + if (!print_colon) + ctx->value = ""; + + log_and_notify_errno( + ctx->conf, file, lineno, "%s:%d: [%s]%s%s%s%s: %s", + ctx->path, ctx->lineno, ctx->section, print_dot ? "." : "", + ctx->key, print_colon ? ": " : "", ctx->value, formatted_msg); + + free(formatted_msg); +} + +#define LOG_CONTEXTUAL_ERR(...) \ + log_contextual(ctx, LOG_CLASS_ERROR, __FILE__, __LINE__, __VA_ARGS__) + +#define LOG_CONTEXTUAL_WARN(...) \ + log_contextual(ctx, LOG_CLASS_WARNING, __FILE__, __LINE__, __VA_ARGS__) + +#define LOG_CONTEXTUAL_ERRNO(...) \ + log_contextual_errno(ctx, __FILE__, __LINE__, __VA_ARGS__) + +#define LOG_AND_NOTIFY_ERR(...) \ + log_and_notify(conf, LOG_CLASS_ERROR, __FILE__, __LINE__, __VA_ARGS__) + +#define LOG_AND_NOTIFY_WARN(...) \ + log_and_notify(conf, LOG_CLASS_WARNING, __FILE__, __LINE__, __VA_ARGS__) + +#define LOG_AND_NOTIFY_ERRNO(...) \ + log_and_notify_errno(conf, __FILE__, __LINE__, __VA_ARGS__) + +static char * +get_shell(void) +{ + const char *shell = getenv("SHELL"); + + if (shell == NULL) { + struct passwd *passwd = getpwuid(getuid()); + if (passwd == NULL) { + LOG_ERRNO("failed to lookup user: falling back to 'sh'"); + shell = "sh"; + } else + shell = passwd->pw_shell; + } + + LOG_DBG("user's shell: %s", shell); + return xstrdup(shell); +} + +struct config_file { + char *path; /* Full, absolute, path */ + int fd; /* FD of file, O_RDONLY */ +}; + +static struct config_file +open_config(void) +{ + char *path = NULL; + struct config_file ret = {.path = NULL, .fd = -1}; + + const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); + const char *xdg_config_dirs = getenv("XDG_CONFIG_DIRS"); + const char *home_dir = getenv("HOME"); + char *xdg_config_dirs_copy = NULL; + + /* First, check XDG_CONFIG_HOME (or .config, if unset) */ + if (xdg_config_home != NULL && xdg_config_home[0] != '\0') + path = xstrjoin(xdg_config_home, "/foot/foot.ini"); + else if (home_dir != NULL) + path = xstrjoin(home_dir, "/.config/foot/foot.ini"); + + if (path != NULL) { + LOG_DBG("checking for %s", path); + int fd = open(path, O_RDONLY | O_CLOEXEC); + + if (fd >= 0) { + ret = (struct config_file) {.path = path, .fd = fd}; + path = NULL; + goto done; + } + } + + xdg_config_dirs_copy = xdg_config_dirs != NULL && xdg_config_dirs[0] != '\0' + ? strdup(xdg_config_dirs) + : strdup("/etc/xdg"); + + if (xdg_config_dirs_copy == NULL || xdg_config_dirs_copy[0] == '\0') + goto done; + + for (const char *conf_dir = strtok(xdg_config_dirs_copy, ":"); + conf_dir != NULL; + conf_dir = strtok(NULL, ":")) + { + free(path); + path = xstrjoin(conf_dir, "/foot/foot.ini"); + + LOG_DBG("checking for %s", path); + int fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd >= 0) { + ret = (struct config_file){.path = path, .fd = fd}; + path = NULL; + goto done; + } + } + +done: + free(xdg_config_dirs_copy); + free(path); + return ret; +} + +static bool +str_has_prefix(const char *str, const char *prefix) +{ + return strncmp(str, prefix, strlen(prefix)) == 0; +} + +static bool NOINLINE +value_to_bool(struct context *ctx, bool *res) +{ + static const char *const yes[] = {"on", "true", "yes", "1"}; + static const char *const no[] = {"off", "false", "no", "0"}; + + for (size_t i = 0; i < ALEN(yes); i++) { + if (strcasecmp(ctx->value, yes[i]) == 0) { + *res = true; + return true; + } + } + + for (size_t i = 0; i < ALEN(no); i++) { + if (strcasecmp(ctx->value, no[i]) == 0) { + *res = false; + return true; + } + } + + LOG_CONTEXTUAL_ERR("invalid boolean value"); + return false; +} + + +static bool NOINLINE +str_to_ulong(const char *s, int base, unsigned long *res) +{ + if (s == NULL) + return false; + + errno = 0; + char *end = NULL; + + unsigned long v = strtoul(s, &end, base); + if (!(errno == 0 && *end == '\0')) + return false; + + *res = v; + return true; +} + +static bool NOINLINE +str_to_uint32(const char *s, int base, uint32_t *res) +{ + unsigned long v; + bool ret = str_to_ulong(s, base, &v); + if (v > UINT32_MAX) + return false; + *res = v; + return ret; +} + +static bool NOINLINE +str_to_uint16(const char *s, int base, uint16_t *res) +{ + unsigned long v; + bool ret = str_to_ulong(s, base, &v); + if (v > UINT16_MAX) + return false; + *res = v; + return ret; +} + +static bool NOINLINE +value_to_uint16(struct context *ctx, int base, uint16_t *res) +{ + if (!str_to_uint16(ctx->value, base, res)) { + LOG_CONTEXTUAL_ERR( + "invalid integer value, or outside range 0-%u", UINT16_MAX); + return false; + } + return true; +} + +static bool NOINLINE +value_to_uint32(struct context *ctx, int base, uint32_t *res) +{ + if (!str_to_uint32(ctx->value, base, res)){ + LOG_CONTEXTUAL_ERR( + "invalid integer value, or outside range 0-%u", UINT32_MAX); + return false; + } + return true; +} + +static bool NOINLINE +value_to_dimensions(struct context *ctx, uint32_t *x, uint32_t *y) +{ + if (sscanf(ctx->value, "%ux%u", x, y) != 2) { + LOG_CONTEXTUAL_ERR("invalid dimensions (must be in the form AxB)"); + return false; + } + + return true; +} + +static bool NOINLINE +value_to_float(struct context *ctx, float *res) +{ + const char *s = ctx->value; + + if (s == NULL) + return false; + + errno = 0; + char *end = NULL; + + float v = strtof(s, &end); + if (!(errno == 0 && *end == '\0')) { + LOG_CONTEXTUAL_ERR("invalid decimal value"); + return false; + } + + *res = v; + return true; +} + +static bool NOINLINE +value_to_str(struct context *ctx, char **res) +{ + char *copy = xstrdup(ctx->value); + char *end = copy + strlen(copy) - 1; + + /* Un-quote + * + * Note: this is very simple; we only support the *entire* value + * being quoted. That is, no mid-value quotes. Both double and + * single quotes are supported. + * + * - key="value" OK + * - key=abc "quote" def NOT OK + * - key='value' OK + * + * Finally, we support escaping the quote character, and the + * escape character itself: + * + * - key="value \"quotes\"" + * - key="backslash: \\" + * + * ONLY the "current" quote character can be escaped: + * + * key="value \'" NOt OK (both backslash and single quote is kept) + */ + + if ((copy[0] == '"' && *end == '"') || + (copy[0] == '\'' && *end == '\'')) + { + const char quote = copy[0]; + *end = '\0'; + + memmove(copy, copy + 1, end - copy); + + /* Un-escape */ + for (char *p = copy; *p != '\0'; p++) { + if (p[0] == '\\' && (p[1] == '\\' || p[1] == quote)) { + memmove(p, p + 1, end - p); + } + } + } + + free(*res); + *res = copy; + return true; +} + +static bool NOINLINE +value_to_wchars(struct context *ctx, char32_t **res) +{ + char32_t *s = ambstoc32(ctx->value); + if (s == NULL) { + LOG_CONTEXTUAL_ERR("not a valid string value"); + return false; + } + + free(*res); + *res = s; + return true; +} + +static bool NOINLINE +value_to_enum(struct context *ctx, const char **value_map, int *res) +{ + size_t str_len = 0; + size_t count = 0; + + for (; value_map[count] != NULL; count++) { + if (strcasecmp(value_map[count], ctx->value) == 0) { + *res = count; + return true; + } + str_len += strlen(value_map[count]); + } + + const size_t size = str_len + count * 4 + 1; + char valid_values[512]; + size_t idx = 0; + xassert(size < sizeof(valid_values)); + + for (size_t i = 0; i < count; i++) + idx += xsnprintf(&valid_values[idx], size - idx, "'%s', ", value_map[i]); + + if (count > 0) + valid_values[idx - 2] = '\0'; + + LOG_CONTEXTUAL_ERR("not one of %s", valid_values); + return false; +} + +static bool NOINLINE +value_to_color(struct context *ctx, uint32_t *result, bool allow_alpha) +{ + uint32_t color; + const size_t len = strlen(ctx->value); + const size_t component_count = len / 2; + + if (!(len == 6 || (allow_alpha && len == 8)) || + !str_to_uint32(ctx->value, 16, &color)) + { + if (allow_alpha) { + LOG_CONTEXTUAL_ERR("color must be in either RGB or ARGB format"); + } else { + LOG_CONTEXTUAL_ERR("color must be in RGB format"); + } + + return false; + } + + if (allow_alpha && component_count == 3) { + /* If user left out the alpha component, assume non-transparency */ + color |= 0xff000000; + } + + *result = color; + return true; +} + +static bool NOINLINE +value_to_two_colors(struct context *ctx, + uint32_t *first, uint32_t *second, bool allow_alpha) +{ + bool ret = false; + const char *original_value = ctx->value; + + /* TODO: do this without strdup() */ + char *value_copy = xstrdup(ctx->value); + const char *first_as_str = strtok(value_copy, " "); + const char *second_as_str = strtok(NULL, " "); + + if (first_as_str == NULL || second_as_str == NULL) { + LOG_CONTEXTUAL_ERR("invalid double color value"); + goto out; + } + + uint32_t a, b; + + ctx->value = first_as_str; + if (!value_to_color(ctx, &a, allow_alpha)) + goto out; + + ctx->value = second_as_str; + if (!value_to_color(ctx, &b, allow_alpha)) + goto out; + + *first = a; + *second = b; + ret = true; + +out: + free(value_copy); + ctx->value = original_value; + return ret; +} + +static bool NOINLINE +value_to_pt_or_px(struct context *ctx, struct pt_or_px *res) +{ + const char *s = ctx->value; + + size_t len = s != NULL ? strlen(s) : 0; + if (len >= 2 && s[len - 2] == 'p' && s[len - 1] == 'x') { + errno = 0; + char *end = NULL; + + long value = strtol(s, &end, 10); + if (!(len > 2 && errno == 0 && end == s + len - 2)) { + LOG_CONTEXTUAL_ERR("invalid px value (must be in the form 12px)"); + return false; + } + res->pt = 0; + res->px = value; + } else { + float value; + if (!value_to_float(ctx, &value)) + return false; + res->pt = value; + res->px = 0; + } + + return true; +} + +static struct config_font_list NOINLINE +value_to_fonts(struct context *ctx) +{ + size_t count = 0; + size_t size = 0; + struct config_font *fonts = NULL; + + char *copy = xstrdup(ctx->value); + for (const char *font = strtok(copy, ","); + font != NULL; + font = strtok(NULL, ",")) + { + /* Trim spaces, strictly speaking not necessary, but looks nice :) */ + while (isspace(font[0])) + font++; + + if (font[0] == '\0') + continue; + + struct config_font font_data; + if (!config_font_parse(font, &font_data)) { + ctx->value = font; + LOG_CONTEXTUAL_ERR("invalid font specification"); + goto err; + } + + if (count + 1 > size) { + size += 4; + fonts = xrealloc(fonts, size * sizeof(fonts[0])); + } + + xassert(count + 1 <= size); + fonts[count++] = font_data; + } + + free(copy); + return (struct config_font_list){.arr = fonts, .count = count}; + +err: + free(copy); + free(fonts); + return (struct config_font_list){.arr = NULL, .count = 0}; +} + +static void NOINLINE +free_argv(struct argv *argv) +{ + if (argv->args == NULL) + return; + for (char **a = argv->args; *a != NULL; a++) + free(*a); + free(argv->args); + argv->args = NULL; +} + +static void NOINLINE +clone_argv(struct argv *dst, const struct argv *src) +{ + if (src->args == NULL) { + dst->args = NULL; + return; + } + + size_t count = 0; + for (char **args = src->args; *args != NULL; args++) + count++; + + dst->args = xmalloc((count + 1) * sizeof(dst->args[0])); + for (char **args_src = src->args, **args_dst = dst->args; + *args_src != NULL; args_src++, + args_dst++) + { + *args_dst = xstrdup(*args_src); + } + dst->args[count] = NULL; +} + +static void +spawn_template_free(struct config_spawn_template *template) +{ + free_argv(&template->argv); +} + +static void +spawn_template_clone(struct config_spawn_template *dst, + const struct config_spawn_template *src) +{ + clone_argv(&dst->argv, &src->argv); +} + +static bool NOINLINE +value_to_spawn_template(struct context *ctx, + struct config_spawn_template *template) +{ + spawn_template_free(template); + + char **argv = NULL; + + if (ctx->value[0] == '"' && ctx->value[1] == '"' && ctx->value[2] == '\0') { + template->argv.args = NULL; + return true; + } + + if (!tokenize_cmdline(ctx->value, &argv)) { + LOG_CONTEXTUAL_ERR("syntax error in command line"); + return false; + } + + template->argv.args = argv; + return true; +} + +static bool parse_config_file( + FILE *f, struct config *conf, const char *path, bool errors_are_fatal); + +static bool +parse_section_main(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + const char *value = ctx->value; + bool errors_are_fatal = ctx->errors_are_fatal; + + if (streq(key, "include")) { + char *_include_path = NULL; + const char *include_path = NULL; + + if (value[0] == '~' && value[1] == '/') { + const char *home_dir = getenv("HOME"); + + if (home_dir == NULL) { + LOG_CONTEXTUAL_ERRNO("failed to expand '~'"); + return false; + } + + _include_path = xstrjoin3(home_dir, "/", value + 2); + include_path = _include_path; + } else + include_path = value; + + if (include_path[0] != '/') { + LOG_CONTEXTUAL_ERR("not an absolute path"); + free(_include_path); + return false; + } + + FILE *include = fopen(include_path, "r"); + + if (include == NULL) { + LOG_CONTEXTUAL_ERRNO("failed to open"); + free(_include_path); + return false; + } + + bool ret = parse_config_file( + include, conf, include_path, errors_are_fatal); + fclose(include); + + LOG_INFO("imported sub-configuration from %s", include_path); + free(_include_path); + return ret; + } + + else if (streq(key, "term")) + return value_to_str(ctx, &conf->term); + + else if (streq(key, "shell")) + return value_to_str(ctx, &conf->shell); + + else if (streq(key, "login-shell")) + return value_to_bool(ctx, &conf->login_shell); + + else if (streq(key, "title")) + return value_to_str(ctx, &conf->title); + + else if (streq(key, "locked-title")) + return value_to_bool(ctx, &conf->locked_title); + + else if (streq(key, "app-id")) + return value_to_str(ctx, &conf->app_id); + + else if (streq(key, "toplevel-tag")) + return value_to_str(ctx, &conf->toplevel_tag); + + else if (streq(key, "initial-window-size-pixels")) { + if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height)) + return false; + + conf->size.type = CONF_SIZE_PX; + return true; + } + + else if (streq(key, "initial-window-size-chars")) { + if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height)) + return false; + + conf->size.type = CONF_SIZE_CELLS; + return true; + } + + else if (streq(key, "pad")) { + unsigned x, y, left, top, right, bottom; + char mode[64] = {0}; + int ret = sscanf(value, "%ux%ux%ux%u %63s", &left, &top, &right, &bottom, mode); + enum center_when center = CENTER_NEVER; + + if (ret == 5) { + if (strcasecmp(mode, "center") == 0) + center = CENTER_ALWAYS; + else if (strcasecmp(mode, "center-when-fullscreen") == 0) + center = CENTER_FULLSCREEN; + else if (strcasecmp(mode, "center-when-maximized-and-fullscreen") == 0) + center = CENTER_MAXIMIZED_AND_FULLSCREEN; + else + center = CENTER_INVALID; + } else if (ret < 4) { + ret = sscanf(value, "%ux%u %63s", &x, &y, mode); + if (ret >= 2) { + left = right = x; + top = bottom = y; + if (ret == 3) { + if (strcasecmp(mode, "center") == 0) + center = CENTER_ALWAYS; + else if (strcasecmp(mode, "center-when-fullscreen") == 0) + center = CENTER_FULLSCREEN; + else if (strcasecmp(mode, "center-when-maximized-and-fullscreen") == 0) + center = CENTER_MAXIMIZED_AND_FULLSCREEN; + else + center = CENTER_INVALID; + } + } + } + + if ((ret < 2 || ret > 5) || center == CENTER_INVALID) { + LOG_CONTEXTUAL_ERR( + "invalid padding (must be in the form RIGHTxTOPxLEFTxBOTTOM or XxY " + "[center|" + "center-when-fullscreen|" + "center-when-maximized-and-fullscreen])"); + return false; + } + + conf->pad_left = left; + conf->pad_top = top; + conf->pad_right = right; + conf->pad_bottom = bottom; + conf->center_when = (ret == 4 || ret == 2) ? CENTER_NEVER : center; + return true; + } + + else if (streq(key, "resize-delay-ms")) + return value_to_uint16(ctx, 10, &conf->resize_delay_ms); + + else if (streq(key, "resize-by-cells")) + return value_to_bool(ctx, &conf->resize_by_cells); + + else if (streq(key, "resize-keep-grid")) + return value_to_bool(ctx, &conf->resize_keep_grid); + + else if (streq(key, "bold-text-in-bright")) { + if (streq(value, "palette-based")) { + conf->bold_in_bright.enabled = true; + conf->bold_in_bright.palette_based = true; + } else { + if (!value_to_bool(ctx, &conf->bold_in_bright.enabled)) + return false; + conf->bold_in_bright.palette_based = false; + } + return true; + } + + else if (streq(key, "initial-window-mode")) { + _Static_assert(sizeof(conf->startup_mode) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"windowed", "maximized", "fullscreen", NULL}, + (int *)&conf->startup_mode); + } + + else if (streq(key, "font") || + streq(key, "font-bold") || + streq(key, "font-italic") || + streq(key, "font-bold-italic")) + + { + size_t idx = + streq(key, "font") ? 0 : + streq(key, "font-bold") ? 1 : + streq(key, "font-italic") ? 2 : 3; + + struct config_font_list new_list = value_to_fonts(ctx); + if (new_list.arr == NULL) + return false; + + config_font_list_destroy(&conf->fonts[idx]); + conf->fonts[idx] = new_list; + return true; + } + + else if (streq(key, "font-size-adjustment")) { + const size_t len = strlen(ctx->value); + if (len >= 1 && ctx->value[len - 1] == '%') { + errno = 0; + char *end = NULL; + + float percent = strtof(ctx->value, &end); + if (!(len > 1 && errno == 0 && end == ctx->value + len - 1)) { + LOG_CONTEXTUAL_ERR( + "invalid percent value (must be in the form 10.5%%)"); + return false; + } + + conf->font_size_adjustment.percent = percent / 100.; + conf->font_size_adjustment.pt_or_px.pt = 0; + conf->font_size_adjustment.pt_or_px.px = 0; + return true; + } else { + bool ret = value_to_pt_or_px(ctx, &conf->font_size_adjustment.pt_or_px); + if (ret) + conf->font_size_adjustment.percent = 0.; + return ret; + } + } + + else if (streq(key, "line-height")) + return value_to_pt_or_px(ctx, &conf->line_height); + + else if (streq(key, "letter-spacing")) + return value_to_pt_or_px(ctx, &conf->letter_spacing); + + else if (streq(key, "horizontal-letter-offset")) + return value_to_pt_or_px(ctx, &conf->horizontal_letter_offset); + + else if (streq(key, "vertical-letter-offset")) + return value_to_pt_or_px(ctx, &conf->vertical_letter_offset); + + else if (streq(key, "underline-offset")) { + if (!value_to_pt_or_px(ctx, &conf->underline_offset)) + return false; + conf->use_custom_underline_offset = true; + return true; + } + + else if (streq(key, "underline-thickness")) + return value_to_pt_or_px(ctx, &conf->underline_thickness); + + else if (streq(key, "strikeout-thickness")) + return value_to_pt_or_px(ctx, &conf->strikeout_thickness); + + else if (streq(key, "dpi-aware")) + return value_to_bool(ctx, &conf->dpi_aware); + + else if (streq(key, "workers")) + return value_to_uint16(ctx, 10, &conf->render_worker_count); + + else if (streq(key, "word-delimiters")) + return value_to_wchars(ctx, &conf->word_delimiters); + + else if (streq(key, "selection-target")) { + _Static_assert(sizeof(conf->selection_target) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"none", "primary", "clipboard", "both", NULL}, + (int *)&conf->selection_target); + } + + else if (streq(key, "box-drawings-uses-font-glyphs")) + return value_to_bool(ctx, &conf->box_drawings_uses_font_glyphs); + + else if (streq(key, "utmp-helper")) { + if (!value_to_str(ctx, &conf->utmp_helper_path)) + return false; + + if (streq(conf->utmp_helper_path, "none")) { + free(conf->utmp_helper_path); + conf->utmp_helper_path = NULL; + } + + return true; + } + + else if (streq(key, "gamma-correct-blending")) + return value_to_bool(ctx, &conf->gamma_correct); + + else if (streq(key, "initial-color-theme")) { + _Static_assert( + sizeof(conf->initial_color_theme) == sizeof(int), + "enum is not 32-bit"); + + if (!value_to_enum(ctx, (const char*[]){ + "dark", "light", "1", "2", NULL}, + (int *)&conf->initial_color_theme)) + return false; + + if (streq(ctx->value, "1")) { + LOG_WARN("%s:%d: [main].initial-color-theme=1 deprecated, " + "use [main].initial-color-theme=dark instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[main].initial-color-theme=1: " + "use [main].initial-color-theme=dark instead")); + + conf->initial_color_theme = COLOR_THEME_DARK; + } + + else if (streq(ctx->value, "2")) { + LOG_WARN("%s:%d: [main].initial-color-theme=2 deprecated, " + "use [main].initial-color-theme=light instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[main].initial-color-theme=2: " + "use [main].initial-color-theme=light instead")); + + conf->initial_color_theme = COLOR_THEME_LIGHT; + } + + return true; + } + + else if (streq(key, "uppercase-regex-insert")) + return value_to_bool(ctx, &conf->uppercase_regex_insert); + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_security(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "osc52")) { + _Static_assert(sizeof(conf->security.osc52) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum( + ctx, + (const char *[]){"disabled", "copy-enabled", "paste-enabled", "enabled", NULL}, + (int *)&conf->security.osc52); + } else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_bell(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "urgent")) + return value_to_bool(ctx, &conf->bell.urgent); + else if (streq(key, "notify")) + return value_to_bool(ctx, &conf->bell.notify); + else if (streq(key, "system")) + return value_to_bool(ctx, &conf->bell.system_bell); + else if (streq(key, "visual")) + return value_to_bool(ctx, &conf->bell.flash); + else if (streq(key, "command")) + return value_to_spawn_template(ctx, &conf->bell.command); + else if (streq(key, "command-focused")) + return value_to_bool(ctx, &conf->bell.command_focused); + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_desktop_notifications(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "command")) + return value_to_spawn_template( + ctx, &conf->desktop_notifications.command); + else if (streq(key, "command-action-argument")) + return value_to_spawn_template( + ctx, &conf->desktop_notifications.command_action_arg); + else if (streq(key, "close")) + return value_to_spawn_template( + ctx, &conf->desktop_notifications.close); + else if (streq(key, "inhibit-when-focused")) + return value_to_bool( + ctx, &conf->desktop_notifications.inhibit_when_focused); + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_scrollback(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + const char *value = ctx->value; + + if (streq(key, "lines")) + return value_to_uint32(ctx, 10, &conf->scrollback.lines); + + else if (streq(key, "indicator-position")) { + _Static_assert( + sizeof(conf->scrollback.indicator.position) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"none", "fixed", "relative", NULL}, + (int *)&conf->scrollback.indicator.position); + } + + else if (streq(key, "indicator-format")) { + if (streq(value, "percentage")) { + conf->scrollback.indicator.format + = SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE; + return true; + } else if (streq(value, "line")) { + conf->scrollback.indicator.format + = SCROLLBACK_INDICATOR_FORMAT_LINENO; + return true; + } else + return value_to_wchars(ctx, &conf->scrollback.indicator.text); + } + + else if (streq(key, "multiplier")) + return value_to_float(ctx, &conf->scrollback.multiplier); + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_url(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "launch")) + return value_to_spawn_template(ctx, &conf->url.launch); + + else if (streq(key, "label-letters")) + return value_to_wchars(ctx, &conf->url.label_letters); + + else if (streq(key, "style")) { + _Static_assert(sizeof(conf->url.style) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum( + ctx, (const char *[]){"none", "single", "double", "curly", "dotted", "dashed", NULL}, + (int *)&conf->url.style); + } + + else if (streq(key, "osc8-underline")) { + _Static_assert(sizeof(conf->url.osc8_underline) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"url-mode", "always", NULL}, + (int *)&conf->url.osc8_underline); + } + + else if (streq(key, "regex")) { + const char *regex = ctx->value; + regex_t preg; + + int r = regcomp(&preg, regex, REG_EXTENDED); + + if (r != 0) { + char err_buf[128]; + regerror(r, &preg, err_buf, sizeof(err_buf)); + LOG_CONTEXTUAL_ERR("invalid regex: %s", err_buf); + return false; + } + + if (preg.re_nsub == 0) { + LOG_CONTEXTUAL_ERR("invalid regex: no marked subexpression(s)"); + regfree(&preg); + return false; + } + + regfree(&conf->url.preg); + free(conf->url.regex); + + conf->url.regex = xstrdup(regex); + conf->url.preg = preg; + return true; + } + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_regex(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + const char *regex_name = + ctx->section_suffix != NULL ? ctx->section_suffix : ""; + + struct custom_regex *regex = NULL; + tll_foreach(conf->custom_regexes, it) { + if (streq(it->item.name, regex_name)) { + regex = &it->item; + break; + } + } + + if (streq(key, "regex")) { + const char *regex_string = ctx->value; + regex_t preg; + + int r = regcomp(&preg, regex_string, REG_EXTENDED); + + if (r != 0) { + char err_buf[128]; + regerror(r, &preg, err_buf, sizeof(err_buf)); + LOG_CONTEXTUAL_ERR("invalid regex: %s", err_buf); + return false; + } + + if (preg.re_nsub == 0) { + LOG_CONTEXTUAL_ERR("invalid regex: no marked subexpression(s)"); + regfree(&preg); + return false; + } + + if (regex == NULL) { + tll_push_back(conf->custom_regexes, + ((struct custom_regex){.name = xstrdup(regex_name)})); + regex = &tll_back(conf->custom_regexes); + } + + regfree(®ex->preg); + free(regex->regex); + + regex->regex = xstrdup(regex_string); + regex->preg = preg; + return true; + } + + else if (streq(key, "launch")) { + struct config_spawn_template launch = {NULL}; + if (!value_to_spawn_template(ctx, &launch)) + return false; + + if (regex == NULL) { + tll_push_back(conf->custom_regexes, + ((struct custom_regex){.name = xstrdup(regex_name)})); + regex = &tll_back(conf->custom_regexes); + } + + spawn_template_free(®ex->launch); + regex->launch = launch; + return true; + } + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool NOINLINE +parse_color_theme(struct context *ctx, struct color_theme *theme) +{ + const char *key = ctx->key; + + size_t key_len = strlen(key); + uint8_t last_digit = (unsigned char)key[key_len - 1] - '0'; + uint32_t *color = NULL; + + if (isdigit(key[0])) { + unsigned long index; + if (!str_to_ulong(key, 0, &index) || index >= ALEN(theme->table)) { + LOG_CONTEXTUAL_ERR( + "invalid color palette index: %s (not in range 0-%zu)", + key, ALEN(theme->table)); + return false; + } + color = &theme->table[index]; + } + + else if (key_len == 8 && str_has_prefix(key, "regular") && last_digit < 8) + color = &theme->table[last_digit]; + + else if (key_len == 7 && str_has_prefix(key, "bright") && last_digit < 8) + color = &theme->table[8 + last_digit]; + + else if (key_len == 4 && str_has_prefix(key, "dim") && last_digit < 8) { + if (!value_to_color(ctx, &theme->dim[last_digit], false)) + return false; + + theme->use_custom.dim |= 1 << last_digit; + return true; + } + + else if (str_has_prefix(key, "sixel") && + ((key_len == 6 && last_digit < 10) || + (key_len == 7 && key[5] == '1' && last_digit < 6))) + { + size_t idx = key_len == 6 ? last_digit : 10 + last_digit; + return value_to_color(ctx, &theme->sixel[idx], false); + } + + else if (streq(key, "flash")) color = &theme->flash; + else if (streq(key, "foreground")) color = &theme->fg; + else if (streq(key, "background")) color = &theme->bg; + else if (streq(key, "selection-foreground")) color = &theme->selection_fg; + else if (streq(key, "selection-background")) color = &theme->selection_bg; + + else if (streq(key, "jump-labels")) { + if (!value_to_two_colors( + ctx, + &theme->jump_label.fg, + &theme->jump_label.bg, + false)) + { + return false; + } + + theme->use_custom.jump_label = true; + return true; + } + + else if (streq(key, "scrollback-indicator")) { + if (!value_to_two_colors( + ctx, + &theme->scrollback_indicator.fg, + &theme->scrollback_indicator.bg, + false)) + { + return false; + } + + theme->use_custom.scrollback_indicator = true; + return true; + } + + else if (streq(key, "search-box-no-match")) { + if (!value_to_two_colors( + ctx, + &theme->search_box.no_match.fg, + &theme->search_box.no_match.bg, + false)) + { + return false; + } + + theme->use_custom.search_box_no_match = true; + return true; + } + + else if (streq(key, "search-box-match")) { + if (!value_to_two_colors( + ctx, + &theme->search_box.match.fg, + &theme->search_box.match.bg, + false)) + { + return false; + } + + theme->use_custom.search_box_match = true; + return true; + } + + else if (streq(key, "cursor")) { + if (!value_to_two_colors( + ctx, + &theme->cursor.text, + &theme->cursor.cursor, + false)) + { + return false; + } + + theme->use_custom.cursor = true; + return true; + } + + else if (streq(key, "urls")) { + if (!value_to_color(ctx, &theme->url, false)) + return false; + + theme->use_custom.url = true; + return true; + } + + else if (streq(key, "alpha")) { + float alpha; + if (!value_to_float(ctx, &alpha)) + return false; + + if (alpha < 0. || alpha > 1.) { + LOG_CONTEXTUAL_ERR("not in range 0.0-1.0"); + return false; + } + + theme->alpha = alpha * 65535.; + return true; + } + + else if (streq(key, "flash-alpha")) { + float alpha; + if (!value_to_float(ctx, &alpha)) + return false; + + if (alpha < 0. || alpha > 1.) { + LOG_CONTEXTUAL_ERR("not in range 0.0-1.0"); + return false; + } + + theme->flash_alpha = alpha * 65535.; + return true; + } + + else if (streq(key, "alpha-mode")) { + _Static_assert(sizeof(theme->alpha_mode) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"default", "matching", "all", NULL}, + (int *)&theme->alpha_mode); + } + + else if (streq(key, "dim-blend-towards")) { + _Static_assert(sizeof(theme->dim_blend_towards) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"black", "white", NULL}, + (int *)&theme->dim_blend_towards); + } + + else if (streq(key, "blur")) + return value_to_bool(ctx, &theme->blur); + + else { + LOG_CONTEXTUAL_ERR("not valid option"); + return false; + } + + uint32_t color_value; + if (!value_to_color(ctx, &color_value, false)) + return false; + + *color = color_value; + return true; +} + +static bool +parse_section_colors_dark(struct context *ctx) +{ + return parse_color_theme(ctx, &ctx->conf->colors_dark); +} + +static bool +parse_section_colors_light(struct context *ctx) +{ + return parse_color_theme(ctx, &ctx->conf->colors_light); +} + +static bool +parse_section_colors(struct context *ctx) +{ + LOG_WARN("%s:%d: [colors]: deprecated; use [colors-dark] instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[colors]: use [colors-dark] instead")); + + return parse_color_theme(ctx, &ctx->conf->colors_dark); +} + +static bool +parse_section_colors2(struct context *ctx) +{ + LOG_WARN("%s:%d: [colors2]: deprecated; use [colors-light] instead", + ctx->path, ctx->lineno); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup("[colors2]: use [colors-light] instead")); + + return parse_color_theme(ctx, &ctx->conf->colors_light); +} + +static bool +parse_section_cursor(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "style")) { + _Static_assert(sizeof(conf->cursor.style) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"block", "underline", "beam", "hollow", NULL}, + (int *)&conf->cursor.style); + } + + else if (streq(key, "unfocused-style")) { + _Static_assert(sizeof(conf->cursor.unfocused_style) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"unchanged", "hollow", "none", NULL}, + (int *)&conf->cursor.unfocused_style); + } + + else if (streq(key, "blink")) + return value_to_bool(ctx, &conf->cursor.blink.enabled); + + else if (streq(key, "blink-rate")) + return value_to_uint32(ctx, 10, &conf->cursor.blink.rate_ms); + + else if (streq(key, "beam-thickness")) + return value_to_pt_or_px(ctx, &conf->cursor.beam_thickness); + + else if (streq(key, "underline-thickness")) + return value_to_pt_or_px(ctx, &conf->cursor.underline_thickness); + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_mouse(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "hide-when-typing")) + return value_to_bool(ctx, &conf->mouse.hide_when_typing); + + else if (streq(key, "alternate-scroll-mode")) + return value_to_bool(ctx, &conf->mouse.alternate_scroll_mode); + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_csd(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "preferred")) { + _Static_assert(sizeof(conf->csd.preferred) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"none", "server", "client", NULL}, + (int *)&conf->csd.preferred); + } + + else if (streq(key, "font")) { + struct config_font_list new_list = value_to_fonts(ctx); + if (new_list.arr == NULL) + return false; + + config_font_list_destroy(&conf->csd.font); + conf->csd.font = new_list; + return true; + } + + else if (streq(key, "color")) { + uint32_t color; + if (!value_to_color(ctx, &color, true)) + return false; + + conf->csd.color.title_set = true; + conf->csd.color.title = color; + return true; + } + + else if (streq(key, "size")) + return value_to_uint16(ctx, 10, &conf->csd.title_height); + + else if (streq(key, "button-width")) + return value_to_uint16(ctx, 10, &conf->csd.button_width); + + else if (streq(key, "button-color")) { + if (!value_to_color(ctx, &conf->csd.color.buttons, true)) + return false; + + conf->csd.color.buttons_set = true; + return true; + } + + else if (streq(key, "button-minimize-color")) { + if (!value_to_color(ctx, &conf->csd.color.minimize, true)) + return false; + + conf->csd.color.minimize_set = true; + return true; + } + + else if (streq(key, "button-maximize-color")) { + if (!value_to_color(ctx, &conf->csd.color.maximize, true)) + return false; + + conf->csd.color.maximize_set = true; + return true; + } + + else if (streq(key, "button-close-color")) { + if (!value_to_color(ctx, &conf->csd.color.quit, true)) + return false; + + conf->csd.color.close_set = true; + return true; + } + + else if (streq(key, "border-color")) { + if (!value_to_color(ctx, &conf->csd.color.border, true)) + return false; + + conf->csd.color.border_set = true; + return true; + } + + else if (streq(key, "border-width")) + return value_to_uint16(ctx, 10, &conf->csd.border_width_visible); + + else if (streq(key, "hide-when-maximized")) + return value_to_bool(ctx, &conf->csd.hide_when_maximized); + + else if (streq(key, "double-click-to-maximize")) + return value_to_bool(ctx, &conf->csd.double_click_to_maximize); + + else { + LOG_CONTEXTUAL_ERR("not a valid action: %s", key); + return false; + } +} + +static bool +parse_section_tabs(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "enabled")) + return value_to_bool(ctx, &conf->tabs.enabled); + + else if (streq(key, "position")) { + _Static_assert(sizeof(conf->tabs.position) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum( + ctx, + (const char *[]){"top", "bottom", NULL}, + (int *)&conf->tabs.position); + } + + else if (streq(key, "style")) { + _Static_assert(sizeof(conf->tabs.style) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum( + ctx, + (const char *[]){"rounded", "square", NULL}, + (int *)&conf->tabs.style); + } + + else if (streq(key, "layout")) { + _Static_assert(sizeof(conf->tabs.layout) == sizeof(int), + "enum is not 32-bit"); + return value_to_enum( + ctx, + (const char *[]){"span", "floating", NULL}, + (int *)&conf->tabs.layout); + } + + else if (streq(key, "height")) + return value_to_uint16(ctx, 10, &conf->tabs.height); + + else if (streq(key, "tab-width")) + return value_to_uint16(ctx, 10, &conf->tabs.tab_width); + + else if (streq(key, "tab-padding")) + return value_to_uint16(ctx, 10, &conf->tabs.tab_padding); + + else if (streq(key, "label-padding")) + return value_to_uint16(ctx, 10, &conf->tabs.label_padding); + + else if (streq(key, "margin")) + return value_to_uint16(ctx, 10, &conf->tabs.margin); + + else if (streq(key, "corner-radius")) + return value_to_uint16(ctx, 10, &conf->tabs.corner_radius); + + else if (streq(key, "background")) + return value_to_color(ctx, &conf->tabs.colors.bg, false); + + else if (streq(key, "foreground")) + return value_to_color(ctx, &conf->tabs.colors.fg, false); + + else if (streq(key, "active-background")) + return value_to_color(ctx, &conf->tabs.colors.active_bg, false); + + else if (streq(key, "active-foreground")) + return value_to_color(ctx, &conf->tabs.colors.active_fg, false); + + else if (streq(key, "unread-color")) + return value_to_color(ctx, &conf->tabs.colors.unread_fg, false); + + else if (streq(key, "unread-indicator")) { + free(conf->tabs.unread_indicator); + conf->tabs.unread_indicator = xstrdup(ctx->value); + return true; + } + + else if (streq(key, "inherit-cwd")) + return value_to_bool(ctx, &conf->tabs.inherit_cwd); + + else { + LOG_CONTEXTUAL_ERR("not a valid tabs option: %s", key); + return false; + } +} + +static void +free_binding_aux(struct binding_aux *aux) +{ + if (!aux->master_copy) + return; + + switch (aux->type) { + case BINDING_AUX_NONE: break; + case BINDING_AUX_PIPE: free_argv(&aux->pipe); break; + case BINDING_AUX_TEXT: free(aux->text.data); break; + case BINDING_AUX_REGEX: free(aux->regex_name); break; + } +} + +static void +free_key_binding(struct config_key_binding *binding) +{ + free_binding_aux(&binding->aux); + tll_free_and_free(binding->modifiers, free); +} + +static void NOINLINE +free_key_binding_list(struct config_key_binding_list *bindings) +{ + struct config_key_binding *binding = &bindings->arr[0]; + + for (size_t i = 0; i < bindings->count; i++, binding++) + free_key_binding(binding); + free(bindings->arr); + + bindings->arr = NULL; + bindings->count = 0; +} + +static void NOINLINE +parse_modifiers(const char *text, size_t len, config_modifier_list_t *modifiers) +{ + tll_free_and_free(*modifiers, free); + + /* Handle "none" separately because e.g. none+shift is nonsense */ + if (strncmp(text, "none", len) == 0) + return; + + char *copy = xstrndup(text, len); + + for (char *ctx = NULL, *key = strtok_r(copy, "+", &ctx); + key != NULL; + key = strtok_r(NULL, "+", &ctx)) + { + tll_push_back(*modifiers, xstrdup(key)); + } + + free(copy); + tll_sort(*modifiers, strcmp); +} + +static int NOINLINE +argv_compare(const struct argv *argv1, const struct argv *argv2) +{ + if (argv1->args == NULL && argv2->args == NULL) + return 0; + + if (argv1->args == NULL) + return -1; + if (argv2->args == NULL) + return 1; + + for (size_t i = 0; ; i++) { + if (argv1->args[i] == NULL && argv2->args[i] == NULL) + return 0; + if (argv1->args[i] == NULL) + return -1; + if (argv2->args[i] == NULL) + return 1; + + int ret = strcmp(argv1->args[i], argv2->args[i]); + if (ret != 0) + return ret; + } + + BUG("unexpected loop break"); + return 1; +} + +static bool NOINLINE +binding_aux_equal(const struct binding_aux *a, + const struct binding_aux *b) +{ + if (a->type != b->type) + return false; + + switch (a->type) { + case BINDING_AUX_NONE: + return true; + + case BINDING_AUX_PIPE: + return argv_compare(&a->pipe, &b->pipe) == 0; + + case BINDING_AUX_TEXT: + return a->text.len == b->text.len && + memcmp(a->text.data, b->text.data, a->text.len) == 0; + + case BINDING_AUX_REGEX: + return streq(a->regex_name, b->regex_name); + } + + BUG("invalid AUX type: %d", a->type); + return false; +} + +static void NOINLINE +remove_from_key_bindings_list(struct config_key_binding_list *bindings, + int action, const struct binding_aux *aux) +{ + size_t remove_first_idx = 0; + size_t remove_count = 0; + + for (size_t i = 0; i < bindings->count; i++) { + struct config_key_binding *binding = &bindings->arr[i]; + + if (binding->action != action) + continue; + + if (binding_aux_equal(&binding->aux, aux)) { + if (remove_count++ == 0) + remove_first_idx = i; + + xassert(remove_first_idx + remove_count - 1 == i); + free_key_binding(binding); + } + } + + if (remove_count == 0) + return; + + size_t move_count = bindings->count - (remove_first_idx + remove_count); + + memmove( + &bindings->arr[remove_first_idx], + &bindings->arr[remove_first_idx + remove_count], + move_count * sizeof(bindings->arr[0])); + bindings->count -= remove_count; +} + +static const struct { + const char *name; + int code; +} button_map[] = { + /* System defined */ + {"BTN_LEFT", BTN_LEFT}, + {"BTN_RIGHT", BTN_RIGHT}, + {"BTN_MIDDLE", BTN_MIDDLE}, + {"BTN_SIDE", BTN_SIDE}, + {"BTN_EXTRA", BTN_EXTRA}, + {"BTN_FORWARD", BTN_FORWARD}, + {"BTN_BACK", BTN_BACK}, + {"BTN_TASK", BTN_TASK}, + + /* Foot custom, to be able to map scroll events to mouse bindings */ + {"BTN_WHEEL_BACK", BTN_WHEEL_BACK}, + {"BTN_WHEEL_FORWARD", BTN_WHEEL_FORWARD}, + {"BTN_WHEEL_LEFT", BTN_WHEEL_LEFT}, + {"BTN_WHEEL_RIGHT", BTN_WHEEL_RIGHT}, +}; + +static int +mouse_button_name_to_code(const char *name) +{ + for (size_t i = 0; i < ALEN(button_map); i++) { + if (streq(button_map[i].name, name)) + return button_map[i].code; + } + return -1; +} + +static const char* +mouse_button_code_to_name(int code) +{ + for (size_t i = 0; i < ALEN(button_map); i++) { + if (code == button_map[i].code) + return button_map[i].name; + } + + return NULL; +} + +static bool NOINLINE +value_to_key_combos(struct context *ctx, int action, + struct binding_aux *aux, + struct config_key_binding_list *bindings, + enum key_binding_type type) +{ + if (strcasecmp(ctx->value, "none") == 0) { + remove_from_key_bindings_list(bindings, action, aux); + return true; + } + + /* Count number of combinations */ + size_t combo_count = 1; + size_t used_combos = 1; /* For error handling */ + for (const char *p = strchr(ctx->value, ' '); + p != NULL; + p = strchr(p + 1, ' ')) + { + combo_count++; + } + + struct config_key_binding new_combos[combo_count]; + + char *copy = xstrdup(ctx->value); + size_t idx = 0; + + for (char *tok_ctx = NULL, *combo = strtok_r(copy, " ", &tok_ctx); + combo != NULL; + combo = strtok_r(NULL, " ", &tok_ctx), + idx++, used_combos++) + { + struct config_key_binding *new_combo = &new_combos[idx]; + new_combo->action = action; + new_combo->aux = *aux; + new_combo->aux.master_copy = idx == 0; +#if 0 + new_combo->aux.type = BINDING_AUX_PIPE; + new_combo->aux.master_copy = idx == 0; + new_combo->aux.pipe = *argv; +#endif + memset(&new_combo->modifiers, 0, sizeof(new_combo->modifiers)); + new_combo->path = ctx->path; + new_combo->lineno = ctx->lineno; + + char *key = strrchr(combo, '+'); + + if (key == NULL) { + /* No modifiers */ + key = combo; + } else { + *key = '\0'; + parse_modifiers(combo, key - combo, &new_combo->modifiers); + key++; /* Skip past the '+' */ + } + + switch (type) { + case KEY_BINDING: + /* Translate key name to symbol */ + new_combo->k.sym = xkb_keysym_from_name(key, 0); + if (new_combo->k.sym == XKB_KEY_NoSymbol) { + LOG_CONTEXTUAL_ERR("not a valid XKB key name: %s", key); + goto err; + } + break; + + case MOUSE_BINDING: { + new_combo->m.count = 1; + + char *_count = strrchr(key, '-'); + if (_count != NULL) { + *_count = '\0'; + _count++; + + errno = 0; + char *end; + unsigned long value = strtoul(_count, &end, 10); + if (_count[0] == '\0' || *end != '\0' || errno != 0) { + if (errno != 0) + LOG_CONTEXTUAL_ERRNO("invalid click count: %s", _count); + else + LOG_CONTEXTUAL_ERR("invalid click count: %s", _count); + goto err; + } + + new_combo->m.count = value; + } + + new_combo->m.button = mouse_button_name_to_code(key); + if (new_combo->m.button < 0) { + LOG_CONTEXTUAL_ERR("invalid mouse button name: %s", key); + goto err; + } + + break; + } + } + + } + + if (idx == 0) { + LOG_CONTEXTUAL_ERR( + "empty binding not allowed (set to 'none' to unmap)"); + free(copy); + return false; + } + + remove_from_key_bindings_list(bindings, action, aux); + + bindings->arr = xrealloc( + bindings->arr, + (bindings->count + combo_count) * sizeof(bindings->arr[0])); + + memcpy(&bindings->arr[bindings->count], + new_combos, + combo_count * sizeof(bindings->arr[0])); + bindings->count += combo_count; + + free(copy); + return true; + +err: + for (size_t i = 0; i < used_combos; i++) + free_key_binding(&new_combos[i]); + free(copy); + return false; +} + +static bool +modifiers_equal(const config_modifier_list_t *mods1, + const config_modifier_list_t *mods2) +{ + if (tll_length(*mods1) != tll_length(*mods2)) + return false; + + size_t count = 0; + tll_foreach(*mods1, it1) { + size_t skip = count; + tll_foreach(*mods2, it2) { + if (skip > 0) { + skip--; + continue; + } + + if (strcmp(it1->item, it2->item) != 0) + return false; + break; + } + + count++; + } + + return true; + /* + * bool shift = mods1->shift == mods2->shift; + * bool alt = mods1->alt == mods2->alt; + * bool ctrl = mods1->ctrl == mods2->ctrl; + * bool super = mods1->super == mods2->super; + * return shift && alt && ctrl && super; + */ +} + +UNITTEST +{ + config_modifier_list_t mods1 = tll_init(); + config_modifier_list_t mods2 = tll_init(); + + tll_push_back(mods1, xstrdup("foo")); + tll_push_back(mods1, xstrdup("bar")); + + tll_push_back(mods2, xstrdup("foo")); + xassert(!modifiers_equal(&mods1, &mods2)); + + tll_push_back(mods2, xstrdup("zoo")); + xassert(!modifiers_equal(&mods1, &mods2)); + + free(tll_pop_back(mods2)); + tll_push_back(mods2, xstrdup("bar")); + xassert(modifiers_equal(&mods1, &mods2)); + + tll_free_and_free(mods1, free); + tll_free_and_free(mods2, free); +} + +static bool +modifiers_disjoint(const config_modifier_list_t *mods1, + const config_modifier_list_t *mods2) +{ + return !modifiers_equal(mods1, mods2); +} + +static char * NOINLINE +modifiers_to_str(const config_modifier_list_t *mods, bool strip_last_plus) +{ + size_t len = tll_length(*mods); /* '+' separator */ + tll_foreach(*mods, it) + len += strlen(it->item); + + char *ret = xmalloc(len + 1); + size_t idx = 0; + tll_foreach(*mods, it) { + idx += snprintf(&ret[idx], len - idx, "%s", it->item); + ret[idx++] = '+'; + } + + if (strip_last_plus) + idx--; + + ret[idx] = '\0'; + return ret; +} + +/* + * Parses a key binding value in the form + * "[cmd-to-exec arg1 arg2] Mods+Key" + * + * and extracts 'cmd-to-exec' and its arguments. + * + * Input: + * - value: raw string, in the form mentioned above + * - cmd: pointer to string to will be allocated and filled with + * 'cmd-to-exec arg1 arg2' + * - argv: point to array of string. Array will be allocated. Will be + * filled with {'cmd-to-exec', 'arg1', 'arg2', NULL} + * + * Returns: + * - ssize_t, number of bytes that were stripped from 'value' to remove the '[]' + * enclosed cmd and its arguments, including any subsequent + * whitespace characters. I.e. if 'value' is "[cmd] BTN_RIGHT", the + * return value is 6 (strlen("[cmd] ")). + * - cmd: allocated string containing "cmd arg1 arg2...". Caller frees. + * - argv: allocated array containing {"cmd", "arg1", "arg2", NULL}. Caller frees. + */ +static ssize_t NOINLINE +pipe_argv_from_value(struct context *ctx, struct argv *argv) +{ + argv->args = NULL; + + if (ctx->value[0] != '[') + return 0; + + const char *pipe_cmd_end = strrchr(ctx->value, ']'); + if (pipe_cmd_end == NULL) { + LOG_CONTEXTUAL_ERR("unclosed '['"); + return -1; + } + + size_t pipe_len = pipe_cmd_end - ctx->value - 1; + char *cmd = xstrndup(&ctx->value[1], pipe_len); + + if (!tokenize_cmdline(cmd, &argv->args)) { + LOG_CONTEXTUAL_ERR("syntax error in command line"); + free(cmd); + return -1; + } + + ssize_t remove_len = pipe_cmd_end + 1 - ctx->value; + ctx->value = pipe_cmd_end + 1; + while (isspace(*ctx->value)) { + ctx->value++; + remove_len++; + } + + free(cmd); + return remove_len; +} + +static ssize_t NOINLINE +regex_name_from_value(struct context *ctx, char **regex_name) +{ + *regex_name = NULL; + + if (ctx->value[0] != '[') + return 0; + + const char *regex_end = strrchr(ctx->value, ']'); + if (regex_end == NULL) { + LOG_CONTEXTUAL_ERR("unclosed '['"); + return -1; + } + + size_t regex_len = regex_end - ctx->value - 1; + *regex_name = xstrndup(&ctx->value[1], regex_len); + + ssize_t remove_len = regex_end + 1 - ctx->value; + ctx->value = regex_end + 1; + while (isspace(*ctx->value)) { + ctx->value++; + remove_len++; + } + + return remove_len; +} + + +static bool NOINLINE +parse_key_binding_section(struct context *ctx, + int action_count, + const char *const action_map[static action_count], + struct config_key_binding_list *bindings) +{ + for (int action = 0; action < action_count; action++) { + if (action_map[action] == NULL) + continue; + + if (!streq(ctx->key, action_map[action])) + continue; + + struct binding_aux aux = {.type = BINDING_AUX_NONE, .master_copy = true}; + + /* TODO: this is ugly... */ + if (action_map == binding_action_map && + action >= BIND_ACTION_PIPE_SCROLLBACK && + action <= BIND_ACTION_PIPE_COMMAND_OUTPUT) + { + ssize_t pipe_remove_len = pipe_argv_from_value(ctx, &aux.pipe); + if (pipe_remove_len <= 0) + return false; + + aux.type = BINDING_AUX_PIPE; + aux.master_copy = true; + } else if (action_map == binding_action_map && + action >= BIND_ACTION_REGEX_LAUNCH && + action <= BIND_ACTION_REGEX_COPY) + { + char *regex_name = NULL; + ssize_t regex_remove_len = regex_name_from_value(ctx, ®ex_name); + if (regex_remove_len <= 0) + return false; + + aux.type = BINDING_AUX_REGEX; + aux.master_copy = true; + aux.regex_name = regex_name; + } + + if (action_map == binding_action_map && + action >= BIND_ACTION_THEME_SWITCH_1 && + action <= BIND_ACTION_THEME_SWITCH_2) + { + const char *use_instead = + action_map[action == BIND_ACTION_THEME_SWITCH_1 + ? BIND_ACTION_THEME_SWITCH_DARK + : BIND_ACTION_THEME_SWITCH_LIGHT]; + + const char *notif = action == BIND_ACTION_THEME_SWITCH_1 + ? "[key-bindings].color-theme-switch-1: use [key-bindings].color-theme-switch-dark instead" + : "[key-bindings].color-theme-switch-2: use [key-bindings].color-theme-switch-light instead"; + + LOG_WARN("%s:%d: [key-bindings].%s: deprecated, use %s instead", + ctx->path, ctx->lineno, + action_map[action], use_instead); + + user_notification_add( + &ctx->conf->notifications, + USER_NOTIFICATION_DEPRECATED, + xstrdup(notif)); + } + + if (!value_to_key_combos(ctx, action, &aux, bindings, KEY_BINDING)) { + free_binding_aux(&aux); + return false; + } + + return true; + } + + LOG_CONTEXTUAL_ERR("not a valid action: %s", ctx->key); + return false; +} + +UNITTEST +{ + enum test_actions { + TEST_ACTION_NONE, + TEST_ACTION_FOO, + TEST_ACTION_BAR, + TEST_ACTION_COUNT, + }; + + const char *const map[] = { + [TEST_ACTION_NONE] = NULL, + [TEST_ACTION_FOO] = "foo", + [TEST_ACTION_BAR] = "bar", + }; + + struct config conf = {0}; + struct config_key_binding_list bindings = {0}; + + struct context ctx = { + .conf = &conf, + .section = "", + .key = "foo", + .value = "Escape", + .path = "", + }; + + /* + * ADD foo=Escape + * + * This verifies we can bind a single key combo to an action. + */ + xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings)); + xassert(bindings.count == 1); + xassert(bindings.arr[0].action == TEST_ACTION_FOO); + xassert(bindings.arr[0].k.sym == XKB_KEY_Escape); + + /* + * ADD bar=Control+g Control+Shift+x + * + * This verifies we can bind multiple key combos to an action. + */ + ctx.key = "bar"; + ctx.value = "Control+g Control+Shift+x"; + xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings)); + xassert(bindings.count == 3); + xassert(bindings.arr[0].action == TEST_ACTION_FOO); + xassert(bindings.arr[1].action == TEST_ACTION_BAR); + xassert(bindings.arr[1].k.sym == XKB_KEY_g); + xassert(tll_length(bindings.arr[1].modifiers) == 1); + xassert(strcmp(tll_front(bindings.arr[1].modifiers), XKB_MOD_NAME_CTRL) == 0); + xassert(bindings.arr[2].action == TEST_ACTION_BAR); + xassert(bindings.arr[2].k.sym == XKB_KEY_x); + xassert(tll_length(bindings.arr[2].modifiers) == 2); + xassert(strcmp(tll_front(bindings.arr[2].modifiers), XKB_MOD_NAME_CTRL) == 0); + xassert(strcmp(tll_back(bindings.arr[2].modifiers), XKB_MOD_NAME_SHIFT) == 0); + + /* + * REPLACE foo with foo=Mod+v Shift+q + * + * This verifies we can update a single-combo action with multiple + * key combos. + */ + ctx.key = "foo"; + ctx.value = "Mod1+v Shift+q"; + xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings)); + xassert(bindings.count == 4); + xassert(bindings.arr[0].action == TEST_ACTION_BAR); + xassert(bindings.arr[1].action == TEST_ACTION_BAR); + xassert(bindings.arr[2].action == TEST_ACTION_FOO); + xassert(bindings.arr[2].k.sym == XKB_KEY_v); + xassert(tll_length(bindings.arr[2].modifiers) == 1); + xassert(strcmp(tll_front(bindings.arr[2].modifiers), XKB_MOD_NAME_ALT) == 0); + xassert(bindings.arr[3].action == TEST_ACTION_FOO); + xassert(bindings.arr[3].k.sym == XKB_KEY_q); + xassert(tll_length(bindings.arr[3].modifiers) == 1); + xassert(strcmp(tll_front(bindings.arr[3].modifiers), XKB_MOD_NAME_SHIFT) == 0); + + /* + * REMOVE bar + */ + ctx.key = "bar"; + ctx.value = "none"; + xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings)); + xassert(bindings.count == 2); + xassert(bindings.arr[0].action == TEST_ACTION_FOO); + xassert(bindings.arr[1].action == TEST_ACTION_FOO); + + /* + * REMOVE foo + */ + ctx.key = "foo"; + ctx.value = "none"; + xassert(parse_key_binding_section(&ctx, ALEN(map), map, &bindings)); + xassert(bindings.count == 0); + + free(bindings.arr); +} + +static bool +parse_section_key_bindings(struct context *ctx) +{ + return parse_key_binding_section( + ctx, + BIND_ACTION_KEY_COUNT, binding_action_map, + &ctx->conf->bindings.key); +} + +static bool +parse_section_search_bindings(struct context *ctx) +{ + return parse_key_binding_section( + ctx, + BIND_ACTION_SEARCH_COUNT, search_binding_action_map, + &ctx->conf->bindings.search); +} + +static bool +parse_section_url_bindings(struct context *ctx) +{ + return parse_key_binding_section( + ctx, + BIND_ACTION_URL_COUNT, url_binding_action_map, + &ctx->conf->bindings.url); +} + +static bool NOINLINE +resolve_key_binding_collisions(struct config *conf, const char *section_name, + const char *const action_map[], + struct config_key_binding_list *bindings, + enum key_binding_type type) +{ + bool ret = true; + + for (size_t i = 1; i < bindings->count; i++) { + enum {COLLISION_NONE, + COLLISION_OVERRIDE, + COLLISION_BINDING} collision_type = COLLISION_NONE; + const struct config_key_binding *collision_binding = NULL; + + struct config_key_binding *binding1 = &bindings->arr[i]; + xassert(binding1->action != BIND_ACTION_NONE); + + const config_modifier_list_t *mods1 = &binding1->modifiers; + + /* Does our modifiers collide with the selection override mods? */ + if (type == MOUSE_BINDING && + !modifiers_disjoint( + mods1, &conf->mouse.selection_override_modifiers)) + { + collision_type = COLLISION_OVERRIDE; + } + + /* Does our binding collide with another binding? */ + for (ssize_t j = i - 1; + collision_type == COLLISION_NONE && j >= 0; + j--) + { + const struct config_key_binding *binding2 = &bindings->arr[j]; + xassert(binding2->action != BIND_ACTION_NONE); + + if (binding2->action == binding1->action && + binding_aux_equal(&binding1->aux, &binding2->aux)) + { + continue; + } + + const config_modifier_list_t *mods2 = &binding2->modifiers; + + bool mods_equal = modifiers_equal(mods1, mods2); + bool sym_equal; + + switch (type) { + case KEY_BINDING: + sym_equal = binding1->k.sym == binding2->k.sym; + break; + + case MOUSE_BINDING: + sym_equal = (binding1->m.button == binding2->m.button && + binding1->m.count == binding2->m.count); + break; + + default: + BUG("unhandled key binding type"); + } + + if (!mods_equal || !sym_equal) + continue; + + collision_binding = binding2; + collision_type = COLLISION_BINDING; + break; + } + + if (collision_type != COLLISION_NONE) { + char *modifier_names = modifiers_to_str(mods1, false); + char sym_name[64]; + + switch (type){ + case KEY_BINDING: + xkb_keysym_get_name(binding1->k.sym, sym_name, sizeof(sym_name)); + break; + + case MOUSE_BINDING: { + const char *button_name = + mouse_button_code_to_name(binding1->m.button); + + if (binding1->m.count > 1) { + snprintf(sym_name, sizeof(sym_name), "%s-%d", + button_name, binding1->m.count); + } else + strcpy(sym_name, button_name); + break; + } + } + + switch (collision_type) { + case COLLISION_NONE: + break; + + case COLLISION_BINDING: { + bool has_pipe = collision_binding->aux.type == BINDING_AUX_PIPE; + LOG_AND_NOTIFY_ERR( + "%s:%d: [%s].%s: %s%s already mapped to '%s%s%s%s'", + binding1->path, binding1->lineno, section_name, + action_map[binding1->action], + modifier_names, sym_name, + action_map[collision_binding->action], + has_pipe ? " [" : "", + has_pipe ? collision_binding->aux.pipe.args[0] : "", + has_pipe ? "]" : ""); + ret = false; + break; + } + + case COLLISION_OVERRIDE: { + char *override_names = modifiers_to_str( + &conf->mouse.selection_override_modifiers, true); + + if (override_names[0] != '\0') + override_names[strlen(override_names) - 1] = '\0'; + + LOG_AND_NOTIFY_ERR( + "%s:%d: [%s].%s: %s%s: " + "modifiers conflict with 'selection-override-modifiers=%s'", + binding1->path != NULL ? binding1->path : "(default)", + binding1->lineno, section_name, + action_map[binding1->action], + modifier_names, sym_name, override_names); + ret = false; + free(override_names); + break; + } + } + + free(modifier_names); + + if (binding1->aux.master_copy && i + 1 < bindings->count) { + struct config_key_binding *next = &bindings->arr[i + 1]; + + if (next->action == binding1->action && + binding_aux_equal(&binding1->aux, &next->aux)) + { + /* Transfer ownership to next binding */ + next->aux.master_copy = true; + binding1->aux.master_copy = false; + } + } + + free_key_binding(binding1); + + /* Remove the most recent binding */ + size_t move_count = bindings->count - (i + 1); + memmove(&bindings->arr[i], &bindings->arr[i + 1], + move_count * sizeof(bindings->arr[0])); + bindings->count--; + + i--; + } + } + + return ret; +} + +static bool +parse_section_mouse_bindings(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + const char *value = ctx->value; + + if (streq(key, "selection-override-modifiers")) { + parse_modifiers( + ctx->value, strlen(value), + &conf->mouse.selection_override_modifiers); + return true; + } + + struct binding_aux aux; + + ssize_t pipe_remove_len = pipe_argv_from_value(ctx, &aux.pipe); + if (pipe_remove_len < 0) + return false; + + aux.type = pipe_remove_len == 0 ? BINDING_AUX_NONE : BINDING_AUX_PIPE; + aux.master_copy = true; + + for (enum bind_action_normal action = 0; + action < BIND_ACTION_COUNT; + action++) + { + if (binding_action_map[action] == NULL) + continue; + + if (!streq(key, binding_action_map[action])) + continue; + + if (!value_to_key_combos( + ctx, action, &aux, &conf->bindings.mouse, MOUSE_BINDING)) + { + free_binding_aux(&aux); + return false; + } + + return true; + } + + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + free_binding_aux(&aux); + return false; +} + +static bool +parse_section_text_bindings(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + const size_t key_len = strlen(key); + + uint8_t *data = xmalloc(key_len + 1); + size_t data_len = 0; + bool esc = false; + + for (size_t i = 0; i < key_len; i++) { + if (key[i] == '\\') { + if (i + 1 >= key_len) { + ctx->value = ""; + LOG_CONTEXTUAL_ERR("trailing backslash"); + goto err; + } + + esc = true; + } + + else if (esc) { + if (key[i] != 'x') { + ctx->value = ""; + LOG_CONTEXTUAL_ERR("invalid escaped character: %c", key[i]); + goto err; + } + if (i + 2 >= key_len) { + ctx->value = ""; + LOG_CONTEXTUAL_ERR("\\x sequence too short"); + goto err; + } + + const uint8_t nib1 = hex2nibble(key[i + 1]); + const uint8_t nib2 = hex2nibble(key[i + 2]); + + if (nib1 >= HEX_DIGIT_INVALID || nib2 >= HEX_DIGIT_INVALID) { + ctx->value = ""; + LOG_CONTEXTUAL_ERR("invalid \\x sequence: \\x%c%c", + key[i + 1], key[i + 2]); + goto err; + } + + data[data_len++] = nib1 << 4 | nib2; + esc = false; + i += 2; + } + + else + data[data_len++] = key[i]; + } + + struct binding_aux aux = { + .type = BINDING_AUX_TEXT, + .text = { + .data = data, /* data is now owned by value_to_key_combos() */ + .len = data_len, + }, + }; + + if (!value_to_key_combos(ctx, BIND_ACTION_TEXT_BINDING, &aux, + &conf->bindings.key, KEY_BINDING)) + { + /* Do *not* free(data) - it is handled by value_to_key_combos() */ + return false; + } + + return true; + +err: + free(data); + return false; +} + +static bool +parse_section_environment(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + /* Check for pre-existing env variable */ + tll_foreach(conf->env_vars, it) { + if (streq(it->item.name, key)) + return value_to_str(ctx, &it->item.value); + } + + /* + * No pre-existing variable - allocate a new one + */ + + char *value = NULL; + if (!value_to_str(ctx, &value)) + return false; + + tll_push_back(conf->env_vars, ((struct env_var){xstrdup(key), value})); + return true; +} + +static bool +parse_section_tweak(struct context *ctx) +{ + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "scaling-filter")) { + static const char *filters[] = { + [FCFT_SCALING_FILTER_NONE] = "none", + [FCFT_SCALING_FILTER_NEAREST] = "nearest", + [FCFT_SCALING_FILTER_BILINEAR] = "bilinear", + + [FCFT_SCALING_FILTER_IMPULSE] = "impulse", + [FCFT_SCALING_FILTER_BOX] = "box", + [FCFT_SCALING_FILTER_LINEAR] = "linear", + [FCFT_SCALING_FILTER_CUBIC] = "cubic", + [FCFT_SCALING_FILTER_GAUSSIAN] = "gaussian", + [FCFT_SCALING_FILTER_LANCZOS2] = "lanczos2", + [FCFT_SCALING_FILTER_LANCZOS3] = "lanczos3", + [FCFT_SCALING_FILTER_LANCZOS3_STRETCHED] = "lanczos3-stretched", + NULL, + }; + + _Static_assert(sizeof(conf->tweak.fcft_filter) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum(ctx, filters, (int *)&conf->tweak.fcft_filter); + } + + else if (streq(key, "overflowing-glyphs")) + return value_to_bool(ctx, &conf->tweak.overflowing_glyphs); + + else if (streq(key, "damage-whole-window")) + return value_to_bool(ctx, &conf->tweak.damage_whole_window); + + else if (streq(key, "grapheme-shaping")) { + if (!value_to_bool(ctx, &conf->tweak.grapheme_shaping)) + return false; + +#if !defined(FOOT_GRAPHEME_CLUSTERING) + if (conf->tweak.grapheme_shaping) { + LOG_CONTEXTUAL_WARN( + "foot was not compiled with support for grapheme shaping"); + conf->tweak.grapheme_shaping = false; + } +#endif + + if (conf->tweak.grapheme_shaping && !conf->can_shape_grapheme) { + LOG_WARN( + "fcft was not compiled with support for grapheme shaping"); + + /* Keep it enabled though - this will cause us to do + * grapheme-clustering at least */ + } + + return true; + } + + else if (streq(key, "grapheme-width-method")) { + _Static_assert(sizeof(conf->tweak.grapheme_width_method) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"wcswidth", "double-width", "max", NULL}, + (int *)&conf->tweak.grapheme_width_method); + } + + else if (streq(key, "render-timer")) { + _Static_assert(sizeof(conf->tweak.render_timer) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"none", "osd", "log", "both", NULL}, + (int *)&conf->tweak.render_timer); + } + + else if (streq(key, "delayed-render-lower")) { + uint32_t ns; + if (!value_to_uint32(ctx, 10, &ns)) + return false; + + if (ns > 16666666) { + LOG_CONTEXTUAL_ERR("timeout must not exceed 16ms"); + return false; + } + + conf->tweak.delayed_render_lower_ns = ns; + return true; + } + + else if (streq(key, "delayed-render-upper")) { + uint32_t ns; + if (!value_to_uint32(ctx, 10, &ns)) + return false; + + if (ns > 16666666) { + LOG_CONTEXTUAL_ERR("timeout must not exceed 16ms"); + return false; + } + + conf->tweak.delayed_render_upper_ns = ns; + return true; + } + + else if (streq(key, "max-shm-pool-size-mb")) { + uint32_t mb; + if (!value_to_uint32(ctx, 10, &mb)) + return false; + + conf->tweak.max_shm_pool_size = min((int32_t)mb * 1024 * 1024, INT32_MAX); + return true; + } + + else if (streq(key, "box-drawing-base-thickness")) + return value_to_float(ctx, &conf->tweak.box_drawing_base_thickness); + + else if (streq(key, "box-drawing-solid-shades")) + return value_to_bool(ctx, &conf->tweak.box_drawing_solid_shades); + + else if (streq(key, "font-monospace-warn")) + return value_to_bool(ctx, &conf->tweak.font_monospace_warn); + + else if (streq(key, "sixel")) + return value_to_bool(ctx, &conf->tweak.sixel); + + else if (streq(key, "dim-amount")) + return value_to_float(ctx, &conf->dim.amount); + + else if (streq(key, "bold-text-in-bright-amount")) + return value_to_float(ctx, &conf->bold_in_bright.amount); + + else if (streq(key, "surface-bit-depth")) { + _Static_assert(sizeof(conf->tweak.surface_bit_depth) == sizeof(int), + "enum is not 32-bit"); + +#if defined(HAVE_PIXMAN_RGBA_16) + return value_to_enum( + ctx, + (const char *[]){"auto", "8-bit", "10-bit", "16-bit", NULL}, + (int *)&conf->tweak.surface_bit_depth); +#else + return value_to_enum( + ctx, + (const char *[]){"auto", "8-bit", "10-bit", NULL}, + (int *)&conf->tweak.surface_bit_depth); +#endif + } + + else if (streq(key, "min-stride-alignment")) + return value_to_uint32(ctx, 10, &conf->tweak.min_stride_alignment); + + else if (streq(key, "pre-apply-damage")) + return value_to_bool(ctx, &conf->tweak.preapply_damage); + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_section_touch(struct context *ctx) { + struct config *conf = ctx->conf; + const char *key = ctx->key; + + if (streq(key, "long-press-delay")) + return value_to_uint32(ctx, 10, &conf->touch.long_press_delay); + + else { + LOG_CONTEXTUAL_ERR("not a valid option: %s", key); + return false; + } +} + +static bool +parse_key_value(char *kv, char **section, const char **key, const char **value) +{ + bool section_is_needed = section != NULL; + + /* Strip leading whitespace */ + while (isspace(kv[0])) + ++kv; + + if (section_is_needed) + *section = "main"; + + if (kv[0] == '=') + return false; + + *key = kv; + *value = NULL; + + size_t kvlen = strlen(kv); + + /* Strip trailing whitespace */ + while (isspace(kv[kvlen - 1])) + kvlen--; + kv[kvlen] = '\0'; + + for (size_t i = 0; i < kvlen; ++i) { + if (kv[i] == '.' && section_is_needed) { + section_is_needed = false; + *section = kv; + kv[i] = '\0'; + if (i == kvlen - 1 || kv[i + 1] == '=') { + *key = NULL; + return false; + } + *key = &kv[i + 1]; + } else if (kv[i] == '=') { + kv[i] = '\0'; + if (i != kvlen - 1) + *value = &kv[i + 1]; + break; + } + } + + if (*value == NULL) + return false; + + /* Strip trailing whitespace from key (leading stripped earlier) */ + { + xassert(!isspace(*key[0])); + + char *end = (char *)*key + strlen(*key) - 1; + while (isspace(end[0])) + end--; + end[1] = '\0'; + } + + /* Strip leading whitespace from value (trailing stripped earlier) */ + while (isspace(*value[0])) + ++*value; + + return true; +} + +enum section { + SECTION_MAIN, + SECTION_SECURITY, + SECTION_BELL, + SECTION_DESKTOP_NOTIFICATIONS, + SECTION_SCROLLBACK, + SECTION_URL, + SECTION_REGEX, + SECTION_COLORS_DARK, + SECTION_COLORS_LIGHT, + SECTION_CURSOR, + SECTION_MOUSE, + SECTION_CSD, + SECTION_KEY_BINDINGS, + SECTION_SEARCH_BINDINGS, + SECTION_URL_BINDINGS, + SECTION_MOUSE_BINDINGS, + SECTION_TEXT_BINDINGS, + SECTION_ENVIRONMENT, + SECTION_TWEAK, + SECTION_TOUCH, + SECTION_TABS, + + /* Deprecated */ + SECTION_COLORS, + SECTION_COLORS2, + + SECTION_COUNT, +}; + +/* Function pointer, called for each key/value line */ +typedef bool (*parser_fun_t)(struct context *ctx); + +static const struct { + parser_fun_t fun; + const char *name; + bool allow_colon_suffix; +} section_info[] = { + [SECTION_MAIN] = {&parse_section_main, "main"}, + [SECTION_SECURITY] = {&parse_section_security, "security"}, + [SECTION_BELL] = {&parse_section_bell, "bell"}, + [SECTION_DESKTOP_NOTIFICATIONS] = {&parse_section_desktop_notifications, "desktop-notifications"}, + [SECTION_SCROLLBACK] = {&parse_section_scrollback, "scrollback"}, + [SECTION_URL] = {&parse_section_url, "url"}, + [SECTION_REGEX] = {&parse_section_regex, "regex", true}, + [SECTION_COLORS_DARK] = {&parse_section_colors_dark, "colors-dark"}, + [SECTION_COLORS_LIGHT] = {&parse_section_colors_light, "colors-light"}, + [SECTION_CURSOR] = {&parse_section_cursor, "cursor"}, + [SECTION_MOUSE] = {&parse_section_mouse, "mouse"}, + [SECTION_CSD] = {&parse_section_csd, "csd"}, + [SECTION_KEY_BINDINGS] = {&parse_section_key_bindings, "key-bindings"}, + [SECTION_SEARCH_BINDINGS] = {&parse_section_search_bindings, "search-bindings"}, + [SECTION_URL_BINDINGS] = {&parse_section_url_bindings, "url-bindings"}, + [SECTION_MOUSE_BINDINGS] = {&parse_section_mouse_bindings, "mouse-bindings"}, + [SECTION_TEXT_BINDINGS] = {&parse_section_text_bindings, "text-bindings"}, + [SECTION_ENVIRONMENT] = {&parse_section_environment, "environment"}, + [SECTION_TWEAK] = {&parse_section_tweak, "tweak"}, + [SECTION_TOUCH] = {&parse_section_touch, "touch"}, + [SECTION_TABS] = {&parse_section_tabs, "tabs"}, + + /* Deprecated */ + [SECTION_COLORS] = {&parse_section_colors, "colors"}, + [SECTION_COLORS2] = {&parse_section_colors2, "colors2"}, +}; + +static_assert(ALEN(section_info) == SECTION_COUNT, "section info array size mismatch"); + +static enum section +str_to_section(char *str, char **suffix) +{ + *suffix = NULL; + + for (enum section section = SECTION_MAIN; section < SECTION_COUNT; ++section) { + const char *name = section_info[section].name; + + if (streq(str, name)) + return section; + + else if (section_info[section].allow_colon_suffix) { + const size_t str_len = strlen(str); + const size_t name_len = strlen(name); + + /* At least "section:" chars? */ + if (str_len > name_len + 1) { + if (strncmp(str, name, name_len) == 0 && str[name_len] == ':') { + str[name_len] = '\0'; + *suffix = &str[name_len + 1]; + return section; + } + } + } + } + return SECTION_COUNT; +} + +static bool +parse_config_file(FILE *f, struct config *conf, const char *path, bool errors_are_fatal) +{ + enum section section = SECTION_MAIN; + + char *_line = NULL; + size_t count = 0; + bool ret = true; + +#define error_or_continue() \ + { \ + if (errors_are_fatal) { \ + ret = false; \ + goto done; \ + } else \ + continue; \ + } + + char *section_name = xstrdup("main"); + char *section_suffix = NULL; + + struct context context = { + .conf = conf, + .section = section_name, + .section_suffix = section_suffix, + .path = path, + .lineno = 0, + .errors_are_fatal = errors_are_fatal, + }; + struct context *ctx = &context; /* For LOG_AND_*() */ + + errno = 0; + ssize_t len; + + while ((len = getline(&_line, &count, f)) != -1) { + context.key = NULL; + context.value = NULL; + context.lineno++; + + char *line = _line; + + /* Strip leading whitespace */ + while (isspace(line[0])) { + line++; + len--; + } + + /* Empty line, or comment */ + if (line[0] == '\0' || line[0] == '#') + continue; + + /* Strip the trailing newline - may be absent on the last line */ + if (line[len - 1] == '\n') + line[--len] = '\0'; + + /* Split up into key/value pair + trailing comment separated by blank */ + char *key_value = line; + char *kv_trailing = &line[len - 1]; + char *comment = &line[1]; + while (comment[1] != '\0') { + if (isblank(comment[0]) && comment[1] == '#') { + comment[1] = '\0'; /* Terminate key/value pair */ + kv_trailing = comment++; + break; + } + comment++; + } + comment++; + + /* Strip trailing whitespace */ + while (isspace(kv_trailing[0])) + kv_trailing--; + kv_trailing[1] = '\0'; + + /* Check for new section */ + if (key_value[0] == '[') { + key_value++; + + if (key_value[0] == ']') { + LOG_CONTEXTUAL_ERR("empty section name"); + section = SECTION_COUNT; + error_or_continue(); + } + + char *end = strchr(key_value, ']'); + + if (end == NULL) { + context.section = key_value; + LOG_CONTEXTUAL_ERR("syntax error: no closing ']'"); + context.section = section_name; + section = SECTION_COUNT; + error_or_continue(); + } + + end[0] = '\0'; + + if (end[1] != '\0') { + context.section = key_value; + LOG_CONTEXTUAL_ERR("section declaration contains trailing " + "characters"); + context.section = section_name; + section = SECTION_COUNT; + error_or_continue(); + } + + char *maybe_section_suffix; + section = str_to_section(key_value, &maybe_section_suffix); + if (section == SECTION_COUNT) { + context.section = key_value; + LOG_CONTEXTUAL_ERR("invalid section name: %s", key_value); + context.section = section_name; + error_or_continue(); + } + + free(section_name); + free(section_suffix); + section_name = xstrdup(key_value); + section_suffix = maybe_section_suffix != NULL ? xstrdup(maybe_section_suffix) : NULL; + context.section = section_name; + context.section_suffix = section_suffix; + + /* Process next line */ + continue; + } + + if (section >= SECTION_COUNT) { + /* Last section name was invalid; ignore all keys in it */ + continue; + } + + if (!parse_key_value(key_value, NULL, &context.key, &context.value)) { + LOG_CONTEXTUAL_ERR("syntax error: key/value pair has no %s", + context.key == NULL ? "key" : "value"); + error_or_continue(); + } + + LOG_DBG("section=%s, key='%s', value='%s', comment='%s'", + section_info[section].name, context.key, context.value, comment); + + xassert(section >= 0 && section < SECTION_COUNT); + + parser_fun_t section_parser = section_info[section].fun; + xassert(section_parser != NULL); + + if (!section_parser(ctx)) + error_or_continue(); + + /* For next iteration of getline() */ + errno = 0; + } + + if (errno != 0) { + LOG_AND_NOTIFY_ERRNO("failed to read from configuration"); + if (errors_are_fatal) + ret = false; + } + +done: + free(section_name); + free(section_suffix); + free(_line); + return ret; +} + +static char * +get_server_socket_path(void) +{ + const char *xdg_runtime = getenv("XDG_RUNTIME_DIR"); + if (xdg_runtime == NULL) + return xstrdup("/tmp/foot.sock"); + + const char *wayland_display = getenv("WAYLAND_DISPLAY"); + if (wayland_display == NULL) { + return xstrjoin(xdg_runtime, "/foot.sock"); + } + + return xasprintf("%s/foot-%s.sock", xdg_runtime, wayland_display); +} + +static config_modifier_list_t +m(const char *text) +{ + config_modifier_list_t ret = tll_init(); + parse_modifiers(text, strlen(text), &ret); + return ret; +} + +static void +add_default_key_bindings(struct config *conf) +{ + const struct config_key_binding bindings[] = { + {BIND_ACTION_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Prior}}}, + {BIND_ACTION_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Prior}}}, + {BIND_ACTION_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Next}}}, + {BIND_ACTION_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Next}}}, + {BIND_ACTION_CLIPBOARD_COPY, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_c}}}, + {BIND_ACTION_CLIPBOARD_COPY, m("none"), {{XKB_KEY_XF86Copy}}}, + {BIND_ACTION_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_v}}}, + {BIND_ACTION_CLIPBOARD_PASTE, m("none"), {{XKB_KEY_XF86Paste}}}, + {BIND_ACTION_PRIMARY_PASTE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Insert}}}, + {BIND_ACTION_SEARCH_START, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_r}}}, + {BIND_ACTION_FONT_SIZE_UP, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_plus}}}, + {BIND_ACTION_FONT_SIZE_UP, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_equal}}}, + {BIND_ACTION_FONT_SIZE_UP, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_Add}}}, + {BIND_ACTION_FONT_SIZE_DOWN, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_minus}}}, + {BIND_ACTION_FONT_SIZE_DOWN, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_Subtract}}}, + {BIND_ACTION_FONT_SIZE_RESET, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_0}}}, + {BIND_ACTION_FONT_SIZE_RESET, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_0}}}, + {BIND_ACTION_SPAWN_TERMINAL, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_n}}}, + {BIND_ACTION_TAB_NEW, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_t}}}, + {BIND_ACTION_TAB_CLOSE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_w}}}, + {BIND_ACTION_TAB_NEXT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Tab}}}, + {BIND_ACTION_TAB_PREV, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Tab}}}, + {BIND_ACTION_TAB_OVERVIEW, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_space}}}, + {BIND_ACTION_SHOW_URLS_LAUNCH, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_o}}}, + {BIND_ACTION_UNICODE_INPUT, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_u}}}, + {BIND_ACTION_PROMPT_PREV, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_z}}}, + {BIND_ACTION_PROMPT_NEXT, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_x}}}, + }; + + conf->bindings.key.count = ALEN(bindings); + conf->bindings.key.arr = xmemdup(bindings, sizeof(bindings)); +} + + +static void +add_default_search_bindings(struct config *conf) +{ + const struct config_key_binding bindings[] = { + {BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Prior}}}, + {BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Prior}}}, + {BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Next}}}, + {BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_KP_Next}}}, + {BIND_ACTION_SEARCH_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_c}}}, + {BIND_ACTION_SEARCH_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_g}}}, + {BIND_ACTION_SEARCH_CANCEL, m("none"), {{XKB_KEY_Escape}}}, + {BIND_ACTION_SEARCH_COMMIT, m("none"), {{XKB_KEY_Return}}}, + {BIND_ACTION_SEARCH_COMMIT, m("none"), {{XKB_KEY_KP_Enter}}}, + {BIND_ACTION_SEARCH_FIND_PREV, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_r}}}, + {BIND_ACTION_SEARCH_FIND_NEXT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_s}}}, + {BIND_ACTION_SEARCH_EDIT_LEFT, m("none"), {{XKB_KEY_Left}}}, + {BIND_ACTION_SEARCH_EDIT_LEFT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_b}}}, + {BIND_ACTION_SEARCH_EDIT_LEFT_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Left}}}, + {BIND_ACTION_SEARCH_EDIT_LEFT_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_b}}}, + {BIND_ACTION_SEARCH_EDIT_RIGHT, m("none"), {{XKB_KEY_Right}}}, + {BIND_ACTION_SEARCH_EDIT_RIGHT, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_f}}}, + {BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Right}}}, + {BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_f}}}, + {BIND_ACTION_SEARCH_EDIT_HOME, m("none"), {{XKB_KEY_Home}}}, + {BIND_ACTION_SEARCH_EDIT_HOME, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_a}}}, + {BIND_ACTION_SEARCH_EDIT_END, m("none"), {{XKB_KEY_End}}}, + {BIND_ACTION_SEARCH_EDIT_END, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_e}}}, + {BIND_ACTION_SEARCH_DELETE_PREV, m("none"), {{XKB_KEY_BackSpace}}}, + {BIND_ACTION_SEARCH_DELETE_PREV_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_BackSpace}}}, + {BIND_ACTION_SEARCH_DELETE_PREV_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_BackSpace}}}, + {BIND_ACTION_SEARCH_DELETE_NEXT, m("none"), {{XKB_KEY_Delete}}}, + {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Delete}}}, + {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_d}}}, + {BIND_ACTION_SEARCH_DELETE_TO_START, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_u}}}, + {BIND_ACTION_SEARCH_DELETE_TO_END, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_k}}}, + {BIND_ACTION_SEARCH_EXTEND_CHAR, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Right}}}, + {BIND_ACTION_SEARCH_EXTEND_WORD, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Right}}}, + {BIND_ACTION_SEARCH_EXTEND_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_w}}}, + {BIND_ACTION_SEARCH_EXTEND_WORD_WS, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_w}}}, + {BIND_ACTION_SEARCH_EXTEND_LINE_DOWN, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Down}}}, + {BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Left}}}, + {BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Left}}}, + {BIND_ACTION_SEARCH_EXTEND_LINE_UP, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Up}}}, + {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_v}}}, + {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_v}}}, + {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_y}}}, + {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m("none"), {{XKB_KEY_XF86Paste}}}, + {BIND_ACTION_SEARCH_PRIMARY_PASTE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Insert}}}, + {BIND_ACTION_SEARCH_TOGGLE_CASE, m(XKB_MOD_NAME_ALT), {{XKB_KEY_c}}}, + {BIND_ACTION_SEARCH_TOGGLE_WHOLE_WORD, m(XKB_MOD_NAME_ALT), {{XKB_KEY_w}}}, + {BIND_ACTION_SEARCH_TOGGLE_REGEX, m(XKB_MOD_NAME_ALT), {{XKB_KEY_r}}}, + {BIND_ACTION_SEARCH_HISTORY_PREV, m("none"), {{XKB_KEY_Up}}}, + {BIND_ACTION_SEARCH_HISTORY_NEXT, m("none"), {{XKB_KEY_Down}}}, + {BIND_ACTION_SEARCH_COMMIT_LINE, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_Return}}}, + }; + + conf->bindings.search.count = ALEN(bindings); + conf->bindings.search.arr = xmemdup(bindings, sizeof(bindings)); +} + +static void +add_default_url_bindings(struct config *conf) +{ + const struct config_key_binding bindings[] = { + {BIND_ACTION_URL_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_c}}}, + {BIND_ACTION_URL_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_g}}}, + {BIND_ACTION_URL_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_d}}}, + {BIND_ACTION_URL_CANCEL, m("none"), {{XKB_KEY_Escape}}}, + {BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL, m("none"), {{XKB_KEY_t}}}, + }; + + conf->bindings.url.count = ALEN(bindings); + conf->bindings.url.arr = xmemdup(bindings, sizeof(bindings)); +} + +static void +add_default_mouse_bindings(struct config *conf) +{ + const struct config_key_binding bindings[] = { + {BIND_ACTION_SCROLLBACK_UP_MOUSE, m("none"), {.m = {BTN_WHEEL_BACK, 1}}}, + {BIND_ACTION_SCROLLBACK_DOWN_MOUSE, m("none"), {.m = {BTN_WHEEL_FORWARD, 1}}}, + {BIND_ACTION_PRIMARY_PASTE, m("none"), {.m = {BTN_MIDDLE, 1}}}, + {BIND_ACTION_SELECT_BEGIN, m("none"), {.m = {BTN_LEFT, 1}}}, + {BIND_ACTION_SELECT_BEGIN_BLOCK, m(XKB_MOD_NAME_CTRL), {.m = {BTN_LEFT, 1}}}, + {BIND_ACTION_SELECT_EXTEND, m("none"), {.m = {BTN_RIGHT, 1}}}, + {BIND_ACTION_SELECT_EXTEND_CHAR_WISE, m(XKB_MOD_NAME_CTRL), {.m = {BTN_RIGHT, 1}}}, + {BIND_ACTION_SELECT_WORD, m("none"), {.m = {BTN_LEFT, 2}}}, + {BIND_ACTION_SELECT_WORD_WS, m(XKB_MOD_NAME_CTRL), {.m = {BTN_LEFT, 2}}}, + {BIND_ACTION_SELECT_QUOTE, m("none"), {.m = {BTN_LEFT, 3}}}, + {BIND_ACTION_SELECT_ROW, m("none"), {.m = {BTN_LEFT, 4}}}, + {BIND_ACTION_FONT_SIZE_UP, m("Control"), {.m = {BTN_WHEEL_BACK, 1}}}, + {BIND_ACTION_FONT_SIZE_DOWN, m("Control"), {.m = {BTN_WHEEL_FORWARD, 1}}}, + }; + + conf->bindings.mouse.count = ALEN(bindings); + conf->bindings.mouse.arr = xmemdup(bindings, sizeof(bindings)); +} + +static void NOINLINE +config_font_list_clone(struct config_font_list *dst, + const struct config_font_list *src) +{ + dst->count = src->count; + dst->arr = xmalloc(dst->count * sizeof(dst->arr[0])); + + for (size_t j = 0; j < dst->count; j++) { + dst->arr[j].pt_size = src->arr[j].pt_size; + dst->arr[j].px_size = src->arr[j].px_size; + dst->arr[j].pattern = xstrdup(src->arr[j].pattern); + } +} + +bool +config_load(struct config *conf, const char *conf_path, + user_notifications_t *initial_user_notifications, + config_override_t *overrides, bool errors_are_fatal, + bool as_server) +{ + bool ret = true; + enum fcft_capabilities fcft_caps = fcft_capabilities(); + + *conf = (struct config) { + .conf_path = (conf_path ? xstrdup(conf_path) : NULL), + .term = xstrdup(FOOT_DEFAULT_TERM), + .shell = get_shell(), + .title = xstrdup("foot"), + .app_id = (as_server ? xstrdup("footclient") : xstrdup("foot")), + .toplevel_tag = xstrdup(""), + .word_delimiters = xc32dup(U",│`|:\"'()[]{}<>"), + .size = { + .type = CONF_SIZE_PX, + .width = 700, + .height = 500, + }, + .pad_left = 0, + .pad_top = 0, + .pad_right = 0, + .pad_bottom = 0, + .center_when = CENTER_MAXIMIZED_AND_FULLSCREEN, + .resize_by_cells = true, + .resize_keep_grid = true, + .resize_delay_ms = 100, + .dim = { .amount = 1.5 }, + .bold_in_bright = { + .enabled = false, + .palette_based = false, + .amount = 1.3, + }, + .startup_mode = STARTUP_WINDOWED, + .fonts = {{0}}, + .font_size_adjustment = {.percent = 0., .pt_or_px = {.pt = 0.5, .px = 0}}, + .line_height = {.pt = 0, .px = -1}, + .letter_spacing = {.pt = 0, .px = 0}, + .horizontal_letter_offset = {.pt = 0, .px = 0}, + .vertical_letter_offset = {.pt = 0, .px = 0}, + .use_custom_underline_offset = false, + .box_drawings_uses_font_glyphs = false, + .underline_thickness = {.pt = 0., .px = -1}, + .strikeout_thickness = {.pt = 0., .px = -1}, + .dpi_aware = false, + .gamma_correct = false, + .uppercase_regex_insert = true, + .security = { + .osc52 = OSC52_ENABLED, + }, + .bell = { + .urgent = false, + .notify = false, + .flash = false, + .system_bell = true, + .command = { + .argv = {.args = NULL}, + }, + .command_focused = false, + }, + .url = { + .label_letters = xc32dup(U"sadfjklewcmpgh"), + .osc8_underline = OSC8_UNDERLINE_URL_MODE, + .style = UNDERLINE_DOTTED, + }, + .custom_regexes = tll_init(), + .can_shape_grapheme = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING, + .scrollback = { + .lines = 1000, + .indicator = { + .position = SCROLLBACK_INDICATOR_POSITION_RELATIVE, + .format = SCROLLBACK_INDICATOR_FORMAT_TEXT, + .text = xc32dup(U""), + }, + .multiplier = 3., + }, + .colors_dark = { + .fg = default_foreground, + .bg = default_background, + .flash = 0x7f7f00, + .flash_alpha = 0x7fff, + .alpha = 0xffff, + .alpha_mode = ALPHA_MODE_DEFAULT, + .dim_blend_towards = DIM_BLEND_TOWARDS_BLACK, + .selection_fg = 0x80000000, /* Use default bg */ + .selection_bg = 0x80000000, /* Use default fg */ + .cursor = { + .text = 0, + .cursor = 0, + }, + .use_custom = { + .jump_label = false, + .scrollback_indicator = false, + .url = false, + }, + .blur = false, + }, + .initial_color_theme = COLOR_THEME_DARK, + .cursor = { + .style = CURSOR_BLOCK, + .unfocused_style = CURSOR_UNFOCUSED_HOLLOW, + .blink = { + .enabled = false, + .rate_ms = 500, + }, + .beam_thickness = {.pt = 1.5}, + .underline_thickness = {.pt = 0., .px = -1}, + }, + .mouse = { + .hide_when_typing = false, + .alternate_scroll_mode = true, + .selection_override_modifiers = tll_init(), + }, + .csd = { + .preferred = CONF_CSD_PREFER_SERVER, + .font = {0}, + .hide_when_maximized = false, + .double_click_to_maximize = true, + .title_height = 26, + .border_width = 5, + .border_width_visible = 0, + .button_width = 26, + }, + + .render_worker_count = sysconf(_SC_NPROCESSORS_ONLN), + .server_socket_path = get_server_socket_path(), + .presentation_timings = false, + .selection_target = SELECTION_TARGET_PRIMARY, + .hold_at_exit = false, + .desktop_notifications = { + .command = { + .argv = {.args = NULL}, + }, + .command_action_arg = { + .argv = {.args = NULL}, + }, + .close = { + .argv = {.args = NULL}, + }, + .inhibit_when_focused = true, + }, + + .tweak = { + .fcft_filter = FCFT_SCALING_FILTER_LANCZOS3, + .overflowing_glyphs = true, +#if defined(FOOT_GRAPHEME_CLUSTERING) && FOOT_GRAPHEME_CLUSTERING + .grapheme_shaping = fcft_caps & FCFT_CAPABILITY_GRAPHEME_SHAPING, +#endif + .grapheme_width_method = GRAPHEME_WIDTH_DOUBLE, + .delayed_render_lower_ns = 500000, /* 0.5ms */ + .delayed_render_upper_ns = 16666666 / 2, /* half a frame period (60Hz) */ + .max_shm_pool_size = 512 * 1024 * 1024, + .render_timer = RENDER_TIMER_NONE, + .damage_whole_window = false, + .box_drawing_base_thickness = 0.04, + .box_drawing_solid_shades = true, + .font_monospace_warn = true, + .sixel = true, + .surface_bit_depth = SHM_BITS_AUTO, + .min_stride_alignment = 256, + .preapply_damage = true, + }, + + .touch = { + .long_press_delay = 400, + }, + + .tabs = { + .enabled = false, + .inherit_cwd = false, + .position = CONF_TABS_POSITION_TOP, + .style = CONF_TABS_STYLE_ROUNDED, + .layout = CONF_TABS_LAYOUT_SPAN, + .height = 26, + .tab_width = 200, + .tab_padding = 8, + .label_padding = 8, + .margin = 4, + .corner_radius = 6, + .unread_indicator = NULL, /* set below to a strdup'd default */ + .colors = { + .bg = 0x1c1c1c, + .fg = 0xb0b0b0, + .active_bg = 0x3a3a3a, + .active_fg = 0xffffff, + .unread_fg = 0xfabd2f, /* warm yellow */ + }, + }, + + .env_vars = tll_init(), +#if defined(UTMP_DEFAULT_HELPER_PATH) + .utmp_helper_path = ((strlen(UTMP_DEFAULT_HELPER_PATH) > 0 && + access(UTMP_DEFAULT_HELPER_PATH, X_OK) == 0) + ? xstrdup(UTMP_DEFAULT_HELPER_PATH) + : NULL), +#endif + .notifications = tll_init(), + }; + + conf->tabs.unread_indicator = xstrdup("●"); + + memcpy(conf->colors_dark.table, default_color_table, sizeof(default_color_table)); + memcpy(conf->colors_dark.sixel, default_sixel_colors, sizeof(default_sixel_colors)); + memcpy(&conf->colors_light, &conf->colors_dark, sizeof(conf->colors_dark)); + conf->colors_light.dim_blend_towards = DIM_BLEND_TOWARDS_WHITE; + + parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); + + tokenize_cmdline( + "notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --hint BOOLEAN:suppress-sound:${muted} --hint STRING:sound-name:${sound-name} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body}", + &conf->desktop_notifications.command.argv.args); + tokenize_cmdline("--action ${action-name}=${action-label}", &conf->desktop_notifications.command_action_arg.argv.args); + tokenize_cmdline("xdg-open ${url}", &conf->url.launch.argv.args); + + { + const char *url_regex_string = + "(" + "(" + "(https?://|mailto:|ftp://|file:|ssh:|ssh://|git://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:)" + "|" + "www\\." + ")" + "(" + /* Safe + reserved + some unsafe characters parenthesis and double quotes omitted (we only allow them when balanced) */ + "[0-9a-zA-Z:/?#@!$&*+,;=.~_%^\\-]+" + "|" + /* Balanced "(...)". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\([]\\[\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\)" + "|" + /* Balanced "[...]". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\[[\\(\\)\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\]" + "|" + /* Balanced '"..."'. Content is same as above, plus all _other_ characters we require to be balanced */ + "\"[]\\[\\(\\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\"" + "|" + /* Balanced "'...'". Content is same as above, plus all _other_ characters we require to be balanced */ + "'[]\\[\\(\\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\\-]*'" + ")+" + "(" + /* Same as above, except :?!,;. are excluded */ + "[0-9a-zA-Z/#@$&*+=~_%^\\-]" + "|" + /* Balanced "(...)". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\([]\\[\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\)" + "|" + /* Balanced "[...]". Content is same as above, plus all _other_ characters we require to be balanced */ + "\\[[\\(\\)\"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\\]" + "|" + /* Balanced '"..."'. Content is same as above, plus all _other_ characters we require to be balanced */ + "\"[]\\[\\(\\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\\-]*\"" + "|" + /* Balanced "'...'". Content is same as above, plus all _other_ characters we require to be balanced */ + "'[]\\[\\(\\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\\-]*'" + ")" + ")"; + + int r = regcomp(&conf->url.preg, url_regex_string, REG_EXTENDED); + xassert(r == 0); + conf->url.regex = xstrdup(url_regex_string); + xassert(conf->url.preg.re_nsub >= 1); + } + + tll_foreach(*initial_user_notifications, it) { + tll_push_back(conf->notifications, it->item); + tll_remove(*initial_user_notifications, it); + } + + add_default_key_bindings(conf); + add_default_search_bindings(conf); + add_default_url_bindings(conf); + add_default_mouse_bindings(conf); + + struct config_file conf_file = {.path = NULL, .fd = -1}; + if (conf_path != NULL) { + int fd = open(conf_path, O_RDONLY); + if (fd < 0) { + LOG_AND_NOTIFY_ERRNO("%s: failed to open", conf_path); + ret = !errors_are_fatal; + } else { + conf_file.path = xstrdup(conf_path); + conf_file.fd = fd; + } + } else { + conf_file = open_config(); + if (conf_file.fd < 0) { + LOG_WARN("no configuration found, using defaults"); + ret = !errors_are_fatal; + } + } + + if (conf_file.path && conf_file.fd >= 0) { + LOG_INFO("loading configuration from %s", conf_file.path); + + FILE *f = fdopen(conf_file.fd, "r"); + if (f == NULL) { + LOG_AND_NOTIFY_ERRNO("%s: failed to open", conf_file.path); + ret = !errors_are_fatal; + } else { + if (!parse_config_file(f, conf, conf_file.path, errors_are_fatal)) + ret = !errors_are_fatal; + + fclose(f); + conf_file.fd = -1; + } + } + + if (!config_override_apply(conf, overrides, errors_are_fatal)) + ret = !errors_are_fatal; + + if (ret && conf->fonts[0].count == 0) { + struct config_font font; + if (!config_font_parse("monospace", &font)) { + LOG_ERR("failed to load font 'monospace' - no fonts installed?"); + ret = false; + } else { + conf->fonts[0].count = 1; + conf->fonts[0].arr = xmalloc(sizeof(font)); + conf->fonts[0].arr[0] = font; + } + } + + if (ret && conf->csd.font.count == 0) + config_font_list_clone(&conf->csd.font, &conf->fonts[0]); + +#if defined(_DEBUG) + for (size_t i = 0; i < conf->bindings.key.count; i++) + xassert(conf->bindings.key.arr[i].action != BIND_ACTION_NONE); + for (size_t i = 0; i < conf->bindings.search.count; i++) + xassert(conf->bindings.search.arr[i].action != BIND_ACTION_SEARCH_NONE); + for (size_t i = 0; i < conf->bindings.url.count; i++) + xassert(conf->bindings.url.arr[i].action != BIND_ACTION_URL_NONE); +#endif + + free(conf_file.path); + if (conf_file.fd >= 0) + close(conf_file.fd); + + return ret; +} + +bool +config_override_apply(struct config *conf, config_override_t *overrides, + bool errors_are_fatal) +{ + char *section_name = NULL; + + struct context context = { + .conf = conf, + .path = "override", + .lineno = 0, + .errors_are_fatal = errors_are_fatal, + }; + struct context *ctx = &context; + + tll_foreach(*overrides, it) { + context.lineno++; + + if (!parse_key_value(it->item, §ion_name, &context.key, &context.value)) + { + LOG_CONTEXTUAL_ERR("syntax error: key/value pair has no %s", + context.key == NULL ? "key" : "value"); + if (errors_are_fatal) + return false; + continue; + } + + if (section_name[0] == '\0') { + LOG_CONTEXTUAL_ERR("empty section name"); + if (errors_are_fatal) + return false; + continue; + } + + char *maybe_section_suffix = NULL; + enum section section = str_to_section(section_name, &maybe_section_suffix); + + context.section = section_name; + context.section_suffix = maybe_section_suffix; + + if (section == SECTION_COUNT) { + LOG_CONTEXTUAL_ERR("invalid section name: %s", section_name); + if (errors_are_fatal) + return false; + continue; + } + + parser_fun_t section_parser = section_info[section].fun; + xassert(section_parser != NULL); + + if (!section_parser(ctx)) { + if (errors_are_fatal) + return false; + continue; + } + } + + conf->csd.border_width = max( + min_csd_border_width, conf->csd.border_width_visible); + + return + resolve_key_binding_collisions( + conf, section_info[SECTION_KEY_BINDINGS].name, + binding_action_map, &conf->bindings.key, KEY_BINDING) && + resolve_key_binding_collisions( + conf, section_info[SECTION_SEARCH_BINDINGS].name, + search_binding_action_map, &conf->bindings.search, KEY_BINDING) && + resolve_key_binding_collisions( + conf, section_info[SECTION_URL_BINDINGS].name, + url_binding_action_map, &conf->bindings.url, KEY_BINDING) && + resolve_key_binding_collisions( + conf, section_info[SECTION_MOUSE_BINDINGS].name, + binding_action_map, &conf->bindings.mouse, MOUSE_BINDING); +} + +static void NOINLINE +key_binding_list_clone(struct config_key_binding_list *dst, + const struct config_key_binding_list *src) +{ + struct argv *last_master_argv = NULL; + uint8_t *last_master_text_data = NULL; + size_t last_master_text_len = 0; + char *last_master_regex_name = NULL; + + dst->count = src->count; + dst->arr = xmalloc(src->count * sizeof(dst->arr[0])); + + for (size_t i = 0; i < src->count; i++) { + const struct config_key_binding *old = &src->arr[i]; + struct config_key_binding *new = &dst->arr[i]; + + *new = *old; + memset(&new->modifiers, 0, sizeof(new->modifiers)); + tll_foreach(old->modifiers, it) + tll_push_back(new->modifiers, xstrdup(it->item)); + + switch (old->aux.type) { + case BINDING_AUX_NONE: + last_master_argv = NULL; + last_master_text_data = NULL; + last_master_text_len = 0; + break; + + case BINDING_AUX_PIPE: + if (old->aux.master_copy) { + clone_argv(&new->aux.pipe, &old->aux.pipe); + last_master_argv = &new->aux.pipe; + } else { + xassert(last_master_argv != NULL); + new->aux.pipe = *last_master_argv; + } + last_master_text_data = NULL; + last_master_text_len = 0; + break; + + case BINDING_AUX_TEXT: + if (old->aux.master_copy) { + const size_t len = old->aux.text.len; + new->aux.text.len = len; + new->aux.text.data = xmemdup(old->aux.text.data, len); + + last_master_text_len = len; + last_master_text_data = new->aux.text.data; + } else { + xassert(last_master_text_data != NULL); + new->aux.text.len = last_master_text_len; + new->aux.text.data = last_master_text_data; + } + last_master_argv = NULL; + break; + + case BINDING_AUX_REGEX: + if (old->aux.master_copy) { + new->aux.regex_name = xstrdup(old->aux.regex_name); + last_master_regex_name = new->aux.regex_name; + } else { + xassert(last_master_regex_name != NULL); + new->aux.regex_name = last_master_regex_name; + } + break; + } + } +} + +struct config * +config_clone(const struct config *old) +{ + struct config *conf = xmalloc(sizeof(*conf)); + *conf = *old; + + conf->conf_path = (old->conf_path ? xstrdup(old->conf_path) : NULL); + conf->term = xstrdup(old->term); + conf->shell = xstrdup(old->shell); + conf->title = xstrdup(old->title); + conf->app_id = xstrdup(old->app_id); + conf->toplevel_tag = xstrdup(old->toplevel_tag); + conf->word_delimiters = xc32dup(old->word_delimiters); + conf->scrollback.indicator.text = xc32dup(old->scrollback.indicator.text); + conf->server_socket_path = xstrdup(old->server_socket_path); + spawn_template_clone(&conf->bell.command, &old->bell.command); + spawn_template_clone(&conf->desktop_notifications.command, + &old->desktop_notifications.command); + spawn_template_clone(&conf->desktop_notifications.command_action_arg, + &old->desktop_notifications.command_action_arg); + spawn_template_clone(&conf->desktop_notifications.close, + &old->desktop_notifications.close); + + for (size_t i = 0; i < ALEN(conf->fonts); i++) + config_font_list_clone(&conf->fonts[i], &old->fonts[i]); + config_font_list_clone(&conf->csd.font, &old->csd.font); + + conf->url.label_letters = xc32dup(old->url.label_letters); + spawn_template_clone(&conf->url.launch, &old->url.launch); + conf->url.regex = xstrdup(old->url.regex); + regcomp(&conf->url.preg, conf->url.regex, REG_EXTENDED); + + memset(&conf->custom_regexes, 0, sizeof(conf->custom_regexes)); + tll_foreach(old->custom_regexes, it) { + const struct custom_regex *old_regex = &it->item; + + tll_push_back(conf->custom_regexes, + ((struct custom_regex){.name = xstrdup(old_regex->name), + .regex = xstrdup(old_regex->regex)})); + + + struct custom_regex *new_regex = &tll_back(conf->custom_regexes); + regcomp(&new_regex->preg, new_regex->regex, REG_EXTENDED); + spawn_template_clone(&new_regex->launch, &old_regex->launch); + } + + key_binding_list_clone(&conf->bindings.key, &old->bindings.key); + key_binding_list_clone(&conf->bindings.search, &old->bindings.search); + key_binding_list_clone(&conf->bindings.url, &old->bindings.url); + key_binding_list_clone(&conf->bindings.mouse, &old->bindings.mouse); + + conf->env_vars.length = 0; + conf->env_vars.head = conf->env_vars.tail = NULL; + + memset(&conf->mouse.selection_override_modifiers, 0, sizeof(conf->mouse.selection_override_modifiers)); + tll_foreach(old->mouse.selection_override_modifiers, it) + tll_push_back(conf->mouse.selection_override_modifiers, xstrdup(it->item)); + + tll_foreach(old->env_vars, it) { + struct env_var copy = { + .name = xstrdup(it->item.name), + .value = xstrdup(it->item.value), + }; + tll_push_back(conf->env_vars, copy); + } + + conf->utmp_helper_path = + old->utmp_helper_path != NULL ? xstrdup(old->utmp_helper_path) : NULL; + + conf->notifications.length = 0; + conf->notifications.head = conf->notifications.tail = 0; + tll_foreach(old->notifications, it) { + char *text = xstrdup(it->item.text); + user_notification_add(&conf->notifications, it->item.kind, text); + } + + return conf; +} + +UNITTEST +{ + struct config original; + user_notifications_t nots = tll_init(); + config_override_t overrides = tll_init(); + + fcft_init(FCFT_LOG_COLORIZE_NEVER, false, FCFT_LOG_CLASS_NONE); + + bool ret = config_load(&original, "/dev/null", ¬s, &overrides, false, false); + xassert(ret); + + //struct config *clone = config_clone(&original); + //xassert(clone != NULL); + //xassert(clone != &original); + + config_free(&original); + //config_free(clone); + //free(clone); + + fcft_fini(); + + tll_free(overrides); + tll_free(nots); +} + +void +config_free(struct config *conf) +{ + free(conf->conf_path); + free(conf->term); + free(conf->shell); + free(conf->title); + free(conf->app_id); + free(conf->toplevel_tag); + free(conf->word_delimiters); + spawn_template_free(&conf->bell.command); + free(conf->scrollback.indicator.text); + spawn_template_free(&conf->desktop_notifications.command); + spawn_template_free(&conf->desktop_notifications.command_action_arg); + spawn_template_free(&conf->desktop_notifications.close); + for (size_t i = 0; i < ALEN(conf->fonts); i++) + config_font_list_destroy(&conf->fonts[i]); + free(conf->server_socket_path); + + config_font_list_destroy(&conf->csd.font); + + free(conf->url.label_letters); + spawn_template_free(&conf->url.launch); + regfree(&conf->url.preg); + free(conf->url.regex); + + tll_foreach(conf->custom_regexes, it) { + struct custom_regex *regex = &it->item; + free(regex->name); + free(regex->regex); + regfree(®ex->preg); + spawn_template_free(®ex->launch); + tll_remove(conf->custom_regexes, it); + } + + free_key_binding_list(&conf->bindings.key); + free_key_binding_list(&conf->bindings.search); + free_key_binding_list(&conf->bindings.url); + free_key_binding_list(&conf->bindings.mouse); + tll_free_and_free(conf->mouse.selection_override_modifiers, free); + + tll_foreach(conf->env_vars, it) { + free(it->item.name); + free(it->item.value); + tll_remove(conf->env_vars, it); + } + + free(conf->utmp_helper_path); + free(conf->tabs.unread_indicator); + user_notifications_free(&conf->notifications); +} + +bool +config_font_parse(const char *pattern, struct config_font *font) +{ + FcPattern *pat = FcNameParse((const FcChar8 *)pattern); + if (pat == NULL) + return false; + + /* + * First look for user specified {pixel}size option + * e.g. "font-name:size=12" + */ + + double pt_size = -1.0; + FcResult have_pt_size = FcPatternGetDouble(pat, FC_SIZE, 0, &pt_size); + + int px_size = -1; + FcResult have_px_size = FcPatternGetInteger(pat, FC_PIXEL_SIZE, 0, &px_size); + + if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) { + /* + * Apply fontconfig config. Can't do that until we've first + * checked for a user provided size, since we may end up with + * both "size" and "pixelsize" being set, and we don't know + * which one takes priority. + */ + FcConfig *fc_conf = FcConfigCreate(); + FcPattern *pat_copy = FcPatternDuplicate(pat); + if (pat_copy == NULL || + !FcConfigSubstitute(fc_conf, pat_copy, FcMatchPattern)) + { + LOG_WARN("%s: failed to do config substitution", pattern); + } else { + have_pt_size = FcPatternGetDouble(pat_copy, FC_SIZE, 0, &pt_size); + have_px_size = FcPatternGetInteger(pat_copy, FC_PIXEL_SIZE, 0, &px_size); + } + + FcPatternDestroy(pat_copy); + FcConfigDestroy(fc_conf); + + if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) + pt_size = 8.0; + } + + FcPatternRemove(pat, FC_SIZE, 0); + FcPatternRemove(pat, FC_PIXEL_SIZE, 0); + + char *stripped_pattern = (char *)FcNameUnparse(pat); + FcPatternDestroy(pat); + + LOG_DBG("%s: pt-size=%.2f, px-size=%d", stripped_pattern, pt_size, px_size); + + if (stripped_pattern == NULL) { + LOG_ERR("failed to convert font pattern to string"); + return false; + } + + *font = (struct config_font){ + .pattern = stripped_pattern, + .pt_size = pt_size, + .px_size = px_size + }; + return true; +} + +void +config_font_list_destroy(struct config_font_list *font_list) +{ + for (size_t i = 0; i < font_list->count; i++) + free(font_list->arr[i].pattern); + free(font_list->arr); + font_list->count = 0; + font_list->arr = NULL; +} + + +bool +check_if_font_is_monospaced(const char *pattern, + user_notifications_t *notifications) +{ + struct fcft_font *f = fcft_from_name( + 1, (const char *[]){pattern}, ":size=8"); + + if (f == NULL) + return true; + + static const char32_t chars[] = {U'a', U'i', U'l', U'M', U'W'}; + + bool is_monospaced = true; + int last_width = -1; + + for (size_t i = 0; i < sizeof(chars) / sizeof(chars[0]); i++) { + const struct fcft_glyph *g = fcft_rasterize_char_utf32( + f, chars[i], FCFT_SUBPIXEL_NONE); + + if (g == NULL) + continue; + + if (last_width >= 0 && g->advance.x != last_width) { + const char *font_name = f->name != NULL + ? f->name + : pattern; + + LOG_WARN("%s: font does not appear to be monospace; " + "check your config, or disable this warning by " + "setting [tweak].font-monospace-warn=no", + font_name); + + static const char fmt[] = + "%s: font does not appear to be monospace; " + "check your config, or disable this warning by " + "setting \033[1m[tweak].font-monospace-warn=no\033[22m"; + + user_notification_add_fmt( + notifications, USER_NOTIFICATION_WARNING, fmt, font_name); + + is_monospaced = false; + break; + } + + last_width = g->advance.x; + } + + fcft_destroy(f); + return is_monospaced; +} + +#if 0 +xkb_mod_mask_t +conf_modifiers_to_mask(const struct seat *seat, + const struct config_key_modifiers *modifiers) +{ + xkb_mod_mask_t mods = 0; + if (seat->kbd.mod_shift != XKB_MOD_INVALID) + mods |= modifiers->shift << seat->kbd.mod_shift; + if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) + mods |= modifiers->ctrl << seat->kbd.mod_ctrl; + if (seat->kbd.mod_alt != XKB_MOD_INVALID) + mods |= modifiers->alt << seat->kbd.mod_alt; + if (seat->kbd.mod_super != XKB_MOD_INVALID) + mods |= modifiers->super << seat->kbd.mod_super; + return mods; +} +#endif diff --git a/config.h b/config.h new file mode 100644 index 0000000..33ea725 --- /dev/null +++ b/config.h @@ -0,0 +1,523 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +#include "user-notification.h" + +#define DEFINE_LIST(type) \ + type##_list { \ + size_t count; \ + type *arr; \ + } + +/* If px != 0 then px is valid, otherwise pt is valid */ +struct pt_or_px { + int16_t px; + float pt; +}; + +struct font_size_adjustment { + struct pt_or_px pt_or_px; + float percent; +}; + +enum cursor_style { CURSOR_BLOCK, CURSOR_UNDERLINE, CURSOR_BEAM, CURSOR_HOLLOW }; +enum cursor_unfocused_style { + CURSOR_UNFOCUSED_UNCHANGED, + CURSOR_UNFOCUSED_HOLLOW, + CURSOR_UNFOCUSED_NONE +}; + +enum conf_size_type {CONF_SIZE_PX, CONF_SIZE_CELLS}; + +struct config_font { + char *pattern; + float pt_size; + int px_size; +}; +DEFINE_LIST(struct config_font); + +#if 0 +struct config_key_modifiers { + bool shift; + bool alt; + bool ctrl; + bool super; +}; +#endif + +struct argv { + char **args; +}; + +enum binding_aux_type { + BINDING_AUX_NONE, + BINDING_AUX_PIPE, + BINDING_AUX_TEXT, + BINDING_AUX_REGEX, +}; + +struct binding_aux { + enum binding_aux_type type; + bool master_copy; + + union { + struct argv pipe; + + struct { + uint8_t *data; + size_t len; + } text; + + char *regex_name; + }; +}; + +enum key_binding_type { + KEY_BINDING, + MOUSE_BINDING, +}; + +typedef tll(char *) config_modifier_list_t; + +struct config_key_binding { + int action; /* One of the various bind_action_* enums from wayland.h */ + //struct config_key_modifiers modifiers; + config_modifier_list_t modifiers; + union { + /* Key bindings */ + struct { + xkb_keysym_t sym; + } k; + + /* Mouse bindings */ + struct { + int button; + int count; + } m; + }; + + struct binding_aux aux; + + /* For error messages in collision handling */ + const char *path; + int lineno; +}; +DEFINE_LIST(struct config_key_binding); + +typedef tll(char *) config_override_t; + +struct config_spawn_template { + struct argv argv; +}; + +struct env_var { + char *name; + char *value; +}; +typedef tll(struct env_var) env_var_list_t; + +struct custom_regex { + char *name; + char *regex; + regex_t preg; + struct config_spawn_template launch; +}; + +struct color_theme { + uint32_t fg; + uint32_t bg; + uint32_t flash; + uint32_t flash_alpha; + uint32_t table[256]; + uint16_t alpha; + uint32_t selection_fg; + uint32_t selection_bg; + uint32_t url; + + uint32_t dim[8]; + uint32_t sixel[16]; + + enum { + DIM_BLEND_TOWARDS_BLACK, + DIM_BLEND_TOWARDS_WHITE, + } dim_blend_towards; + + enum { + ALPHA_MODE_DEFAULT, + ALPHA_MODE_MATCHING, + ALPHA_MODE_ALL + } alpha_mode; + + struct { + uint32_t text; + uint32_t cursor; + } cursor; + + struct { + uint32_t fg; + uint32_t bg; + } jump_label; + + struct { + uint32_t fg; + uint32_t bg; + } scrollback_indicator; + + struct { + struct { + uint32_t fg; + uint32_t bg; + } no_match; + + struct { + uint32_t fg; + uint32_t bg; + } match; + } search_box; + + struct { + bool cursor:1; + bool jump_label:1; + bool scrollback_indicator:1; + bool url:1; + bool search_box_no_match:1; + bool search_box_match:1; + uint8_t dim; + } use_custom; + + bool blur; +}; + +enum which_color_theme { + COLOR_THEME_DARK, + COLOR_THEME_LIGHT, + COLOR_THEME_1, /* Deprecated */ + COLOR_THEME_2, /* Deprecated */ +}; + +enum shm_bit_depth { + SHM_BITS_AUTO, + SHM_BITS_8, + SHM_BITS_10, + SHM_BITS_16, +}; + +enum center_when { + CENTER_INVALID, + CENTER_NEVER, + CENTER_FULLSCREEN, + CENTER_MAXIMIZED_AND_FULLSCREEN, + CENTER_ALWAYS, +}; + +enum underline_style { + UNDERLINE_NONE, + UNDERLINE_SINGLE, /* Legacy underline */ + UNDERLINE_DOUBLE, + UNDERLINE_CURLY, + UNDERLINE_DOTTED, + UNDERLINE_DASHED, +}; + +struct config { + char *conf_path; + char *term; + char *shell; + char *title; + char *app_id; + char *toplevel_tag; + char32_t *word_delimiters; + bool login_shell; + bool locked_title; + + struct { + enum conf_size_type type; + uint32_t width; + uint32_t height; + } size; + + unsigned pad_left; + unsigned pad_top; + unsigned pad_right; + unsigned pad_bottom; + enum center_when center_when; + + bool resize_by_cells; + bool resize_keep_grid; + + uint16_t resize_delay_ms; + + struct { + float amount; + } dim; + + struct { + bool enabled; + bool palette_based; + float amount; + } bold_in_bright; + + enum { STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN } startup_mode; + + bool dpi_aware; + bool gamma_correct; + bool uppercase_regex_insert; + struct config_font_list fonts[4]; + struct font_size_adjustment font_size_adjustment; + + /* Custom font metrics (-1 = use real font metrics) */ + struct pt_or_px line_height; + struct pt_or_px letter_spacing; + + /* Adjusted letter x/y offsets */ + struct pt_or_px horizontal_letter_offset; + struct pt_or_px vertical_letter_offset; + + bool use_custom_underline_offset; + struct pt_or_px underline_offset; + struct pt_or_px underline_thickness; + + struct pt_or_px strikeout_thickness; + + bool box_drawings_uses_font_glyphs; + bool can_shape_grapheme; + + struct { + enum { + OSC52_DISABLED, + OSC52_COPY_ENABLED, + OSC52_PASTE_ENABLED, + OSC52_ENABLED, + } osc52; + } security; + + struct { + bool urgent; + bool notify; + bool flash; + bool system_bell; + struct config_spawn_template command; + bool command_focused; + } bell; + + struct { + uint32_t lines; + + struct { + enum { + SCROLLBACK_INDICATOR_POSITION_NONE, + SCROLLBACK_INDICATOR_POSITION_FIXED, + SCROLLBACK_INDICATOR_POSITION_RELATIVE + } position; + + enum { + SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE, + SCROLLBACK_INDICATOR_FORMAT_LINENO, + SCROLLBACK_INDICATOR_FORMAT_TEXT, + } format; + + char32_t *text; + } indicator; + float multiplier; + } scrollback; + + struct { + char32_t *label_letters; + struct config_spawn_template launch; + enum { + OSC8_UNDERLINE_URL_MODE, + OSC8_UNDERLINE_ALWAYS, + } osc8_underline; + enum underline_style style; + + char *regex; + regex_t preg; + } url; + + tll(struct custom_regex) custom_regexes; + + struct color_theme colors_dark; + struct color_theme colors_light; + enum which_color_theme initial_color_theme; + + struct { + enum cursor_style style; + enum cursor_unfocused_style unfocused_style; + struct { + bool enabled; + uint32_t rate_ms; + } blink; + struct pt_or_px beam_thickness; + struct pt_or_px underline_thickness; + } cursor; + + struct { + bool hide_when_typing; + bool alternate_scroll_mode; + //struct config_key_modifiers selection_override_modifiers; + config_modifier_list_t selection_override_modifiers; + } mouse; + + struct { + /* Bindings for "normal" mode */ + struct config_key_binding_list key; + struct config_key_binding_list mouse; + + /* + * Special modes + */ + + /* While searching (not - action to *start* a search is in the + * 'key' bindings above */ + struct config_key_binding_list search; + + /* While showing URL jump labels */ + struct config_key_binding_list url; + } bindings; + + struct { + enum { CONF_CSD_PREFER_NONE, CONF_CSD_PREFER_SERVER, CONF_CSD_PREFER_CLIENT } preferred; + + uint16_t title_height; + uint16_t border_width; + uint16_t border_width_visible; + uint16_t button_width; + + bool hide_when_maximized; + bool double_click_to_maximize; + + struct { + bool title_set:1; + bool buttons_set:1; + bool minimize_set:1; + bool maximize_set:1; + bool close_set:1; + bool border_set:1; + uint32_t title; + uint32_t buttons; + uint32_t minimize; + uint32_t maximize; + uint32_t quit; /* 'close' collides with #define in epoll-shim */ + uint32_t border; + } color; + + struct config_font_list font; + } csd; + + uint16_t render_worker_count; + char *server_socket_path; + bool presentation_timings; + bool hold_at_exit; + enum { + SELECTION_TARGET_NONE, + SELECTION_TARGET_PRIMARY, + SELECTION_TARGET_CLIPBOARD, + SELECTION_TARGET_BOTH + } selection_target; + + struct { + struct config_spawn_template command; + struct config_spawn_template command_action_arg; + struct config_spawn_template close; + bool inhibit_when_focused; + } desktop_notifications; + + env_var_list_t env_vars; + + char *utmp_helper_path; + + struct { + enum fcft_scaling_filter fcft_filter; + bool overflowing_glyphs; + bool grapheme_shaping; + enum { + GRAPHEME_WIDTH_WCSWIDTH, + GRAPHEME_WIDTH_DOUBLE, + GRAPHEME_WIDTH_MAX, + } grapheme_width_method; + enum { + RENDER_TIMER_NONE, + RENDER_TIMER_OSD, + RENDER_TIMER_LOG, + RENDER_TIMER_BOTH + } render_timer; + bool damage_whole_window; + uint32_t delayed_render_lower_ns; + uint32_t delayed_render_upper_ns; + off_t max_shm_pool_size; + float box_drawing_base_thickness; + bool box_drawing_solid_shades; + bool font_monospace_warn; + bool sixel; + enum shm_bit_depth surface_bit_depth; + uint32_t min_stride_alignment; + bool preapply_damage; + } tweak; + + struct { + uint32_t long_press_delay; + } touch; + + struct { + bool enabled; + enum { + CONF_TABS_POSITION_TOP, + CONF_TABS_POSITION_BOTTOM, + } position; + enum { + CONF_TABS_STYLE_ROUNDED, + CONF_TABS_STYLE_SQUARE, + } style; + enum { + CONF_TABS_LAYOUT_SPAN, + CONF_TABS_LAYOUT_FLOATING, + } layout; + uint16_t height; /* pill height; bar = height + margin in floating mode */ + uint16_t tab_width; /* max tab width in floating mode */ + uint16_t tab_padding; /* gap between tabs in floating mode */ + uint16_t label_padding; /* horizontal padding around the label inside each tab pill */ + uint16_t margin; /* edge gap in floating mode (added to bar height) */ + uint16_t corner_radius; + bool inherit_cwd; + char *unread_indicator; /* UTF-8 string drawn before label when unread; NULL or empty disables */ + struct { + uint32_t bg; + uint32_t fg; + uint32_t active_bg; + uint32_t active_fg; + uint32_t unread_fg; + } colors; + } tabs; + + user_notifications_t notifications; +}; + +bool config_override_apply(struct config *conf, config_override_t *overrides, + bool errors_are_fatal); +bool config_load( + struct config *conf, const char *path, + user_notifications_t *initial_user_notifications, + config_override_t *overrides, bool errors_are_fatal, + bool as_server); +void config_free(struct config *conf); +struct config *config_clone(const struct config *old); + +bool config_font_parse(const char *pattern, struct config_font *font); +void config_font_list_destroy(struct config_font_list *font_list); + +#if 0 +struct seat; +xkb_mod_mask_t +conf_modifiers_to_mask( + const struct seat *seat, const struct config_key_modifiers *modifiers); +#endif +bool check_if_font_is_monospaced( + const char *pattern, user_notifications_t *notifications); diff --git a/csi.c b/csi.c new file mode 100644 index 0000000..87af215 --- /dev/null +++ b/csi.c @@ -0,0 +1,2234 @@ +#include "csi.h" + +#include +#include +#include + +#if defined(_DEBUG) + #include +#endif + +#include + +#define LOG_MODULE "csi" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "char32.h" +#include "config.h" +#include "debug.h" +#include "grid.h" +#include "selection.h" +#include "sixel.h" +#include "util.h" +#include "version.h" +#include "vt.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +#define UNHANDLED() LOG_DBG("unhandled: %s", csi_as_string(term, final, -1)) +#define UNHANDLED_SGR(idx) LOG_DBG("unhandled: %s", csi_as_string(term, 'm', idx)) + +static void +sgr_reset(struct terminal *term) +{ + term->vt.attrs = (struct attributes){0}; + term->vt.underline = (struct underline_range_data){0}; + + term->bits_affecting_ascii_printer.underline_style = false; + term->bits_affecting_ascii_printer.underline_color = false; + term_update_ascii_printer(term); +} + +static const char * +csi_as_string(struct terminal *term, uint8_t final, int idx) +{ + static char msg[1024]; + int c = snprintf(msg, sizeof(msg), "CSI: "); + + for (size_t i = idx >= 0 ? idx : 0; + i < (idx >= 0 ? idx + 1 : term->vt.params.idx); + i++) + { + c += snprintf(&msg[c], sizeof(msg) - c, "%u", + term->vt.params.v[i].value); + + for (size_t j = 0; j < term->vt.params.v[i].sub.idx; j++) { + c += snprintf(&msg[c], sizeof(msg) - c, ":%u", + term->vt.params.v[i].sub.value[j]); + } + + c += snprintf(&msg[c], sizeof(msg) - c, "%s", + i == term->vt.params.idx - 1 ? "" : ";"); + } + + for (size_t i = 0; i < sizeof(term->vt.private); i++) { + char value = (term->vt.private >> (i * 8)) & 0xff; + if (value == 0) + break; + c += snprintf(&msg[c], sizeof(msg) - c, "%c", value); + } + + snprintf(&msg[c], sizeof(msg) - c, "%c (%u parameters)", + final, idx >= 0 ? 1 : term->vt.params.idx); + return msg; +} + +static void +csi_sgr(struct terminal *term) +{ + if (term->vt.params.idx == 0) { + sgr_reset(term); + return; + } + + for (size_t i = 0; i < term->vt.params.idx; i++) { + const int param = term->vt.params.v[i].value; + + switch (param) { + case 0: + sgr_reset(term); + break; + + case 1: term->vt.attrs.bold = true; break; + case 2: term->vt.attrs.dim = true; break; + case 3: term->vt.attrs.italic = true; break; + case 4: { + term->vt.attrs.underline = true; + term->vt.underline.style = UNDERLINE_SINGLE; + + if (unlikely(term->vt.params.v[i].sub.idx == 1)) { + enum underline_style style = term->vt.params.v[i].sub.value[0]; + + switch (style) { + default: + case UNDERLINE_NONE: + term->vt.attrs.underline = false; + term->vt.underline.style = UNDERLINE_NONE; + term->bits_affecting_ascii_printer.underline_style = false; + break; + + case UNDERLINE_SINGLE: + case UNDERLINE_DOUBLE: + case UNDERLINE_CURLY: + case UNDERLINE_DOTTED: + case UNDERLINE_DASHED: + term->vt.underline.style = style; + term->bits_affecting_ascii_printer.underline_style = + style > UNDERLINE_SINGLE; + break; + } + } else + term->bits_affecting_ascii_printer.underline_style = false; + term_update_ascii_printer(term); + break; + } + case 5: term->vt.attrs.blink = true; break; + case 6: LOG_WARN("ignored: rapid blink"); break; + case 7: term->vt.attrs.reverse = true; break; + case 8: term->vt.attrs.conceal = true; break; + case 9: term->vt.attrs.strikethrough = true; break; + + case 21: + term->vt.attrs.underline = true; + term->vt.underline.style = UNDERLINE_DOUBLE; + term->bits_affecting_ascii_printer.underline_style = true; + term_update_ascii_printer(term); + break; + + case 22: term->vt.attrs.bold = term->vt.attrs.dim = false; break; + case 23: term->vt.attrs.italic = false; break; + case 24: { + term->vt.attrs.underline = false; + term->vt.underline.style = UNDERLINE_NONE; + term->bits_affecting_ascii_printer.underline_style = false; + term_update_ascii_printer(term); + break; + } + case 25: term->vt.attrs.blink = false; break; + case 26: break; /* rapid blink, ignored */ + case 27: term->vt.attrs.reverse = false; break; + case 28: term->vt.attrs.conceal = false; break; + case 29: term->vt.attrs.strikethrough = false; break; + + /* Regular foreground colors */ + case 30: + case 31: + case 32: + case 33: + case 34: + case 35: + case 36: + case 37: + term->vt.attrs.fg_src = COLOR_BASE16; + term->vt.attrs.fg = param - 30; + break; + + case 38: + case 48: + case 58: { + uint32_t color; + enum color_source src; + + /* Indexed: 38;5; */ + if (term->vt.params.idx - i - 1 >= 2 && + term->vt.params.v[i + 1].value == 5) + { + src = COLOR_BASE256; + color = min(term->vt.params.v[i + 2].value, + ALEN(term->colors.table) - 1); + i += 2; + } + + /* RGB: 38;2;;; */ + else if (term->vt.params.idx - i - 1 >= 4 && + term->vt.params.v[i + 1].value == 2) + { + uint8_t r = term->vt.params.v[i + 2].value; + uint8_t g = term->vt.params.v[i + 3].value; + uint8_t b = term->vt.params.v[i + 4].value; + src = COLOR_RGB; + color = r << 16 | g << 8 | b; + i += 4; + } + + /* Indexed: 38:5: */ + else if (term->vt.params.v[i].sub.idx >= 2 && + term->vt.params.v[i].sub.value[0] == 5) + { + src = COLOR_BASE256; + color = min(term->vt.params.v[i].sub.value[1], + ALEN(term->colors.table) - 1); + } + + /* + * RGB: 38:2::r:g:b[:ignored:tolerance:tolerance-color-space] + * RGB: 38:2:r:g:b + * + * The second version is a "bastard" version - many + * programs "forget" the color space ID + * parameter... *sigh* + */ + else if (term->vt.params.v[i].sub.idx >= 4 && + term->vt.params.v[i].sub.value[0] == 2) + { + const struct vt_param *param = &term->vt.params.v[i]; + bool have_color_space_id = param->sub.idx >= 5; + + /* 0 - color space (ignored) */ + int r_idx = 2 - !have_color_space_id; + int g_idx = 3 - !have_color_space_id; + int b_idx = 4 - !have_color_space_id; + /* 5 - unused */ + /* 6 - CS tolerance */ + /* 7 - color space associated with tolerance */ + + uint8_t r = param->sub.value[r_idx]; + uint8_t g = param->sub.value[g_idx]; + uint8_t b = param->sub.value[b_idx]; + + src = COLOR_RGB; + color = r << 16 | g << 8 | b; + } + + /* Transparent: 38:1 */ + /* CMY: 38:3::c:m:y[:tolerance:tolerance-color-space] */ + /* CMYK: 38:4::c:m:y:k[:tolerance:tolerance-color-space] */ + + /* Unrecognized */ + else { + UNHANDLED_SGR(i); + break; + } + + if (unlikely(param == 58)) { + term->vt.underline.color_src = src; + term->vt.underline.color = color; + term->bits_affecting_ascii_printer.underline_color = true; + term_update_ascii_printer(term); + } else if (param == 38) { + term->vt.attrs.fg_src = src; + term->vt.attrs.fg = color; + } else { + xassert(param == 48); + term->vt.attrs.bg_src = src; + term->vt.attrs.bg = color; + } + break; + } + + case 39: + term->vt.attrs.fg_src = COLOR_DEFAULT; + break; + + /* Regular background colors */ + case 40: + case 41: + case 42: + case 43: + case 44: + case 45: + case 46: + case 47: + term->vt.attrs.bg_src = COLOR_BASE16; + term->vt.attrs.bg = param - 40; + break; + + case 49: + term->vt.attrs.bg_src = COLOR_DEFAULT; + break; + + case 59: + term->vt.underline.color_src = COLOR_DEFAULT; + term->vt.underline.color = 0; + term->bits_affecting_ascii_printer.underline_color = false; + term_update_ascii_printer(term); + break; + + /* Bright foreground colors */ + case 90: + case 91: + case 92: + case 93: + case 94: + case 95: + case 96: + case 97: + term->vt.attrs.fg_src = COLOR_BASE16; + term->vt.attrs.fg = param - 90 + 8; + break; + + /* Bright background colors */ + case 100: + case 101: + case 102: + case 103: + case 104: + case 105: + case 106: + case 107: + term->vt.attrs.bg_src = COLOR_BASE16; + term->vt.attrs.bg = param - 100 + 8; + break; + + default: + UNHANDLED_SGR(i); + break; + } + } +} + +static void +decset_decrst(struct terminal *term, unsigned param, bool enable) +{ +#if defined(_DEBUG) + /* For UNHANDLED() */ + int UNUSED final = enable ? 'h' : 'l'; +#endif + + /* Note: update XTSAVE/XTRESTORE if adding/removing things here */ + + switch (param) { + case 1: + /* DECCKM */ + term->cursor_keys_mode = + enable ? CURSOR_KEYS_APPLICATION : CURSOR_KEYS_NORMAL; + break; + + case 5: + /* DECSCNM */ + term->reverse = enable; + term_damage_all(term); + term_damage_margins(term); + break; + + case 6: { + /* DECOM */ + term->origin = enable ? ORIGIN_RELATIVE : ORIGIN_ABSOLUTE; + term_cursor_home(term); + break; + } + + case 7: + /* DECAWM */ + term->auto_margin = enable; + term->grid->cursor.lcf = false; + break; + + case 9: + if (enable) + LOG_WARN("unimplemented: X10 mouse tracking mode"); +#if 0 + else if (term->mouse_tracking == MOUSE_X10) + term->mouse_tracking = MOUSE_NONE; +#endif + break; + + case 12: + term->cursor_blink.decset = enable; + term_cursor_blink_update(term); + break; + + case 25: + /* DECTCEM */ + term->hide_cursor = !enable; + break; + + case 45: + term->reverse_wrap = enable; + break; + + case 66: + /* DECNKM */ + term->keypad_keys_mode = enable ? KEYPAD_APPLICATION : KEYPAD_NUMERICAL; + break; + + case 67: + if (enable) + LOG_WARN("unimplemented: DECBKM"); + break; + + case 80: + term->sixel.scrolling = !enable; + break; + + case 1000: + if (enable) + term->mouse_tracking = MOUSE_CLICK; + else if (term->mouse_tracking == MOUSE_CLICK) + term->mouse_tracking = MOUSE_NONE; + term_xcursor_update(term); + break; + + case 1001: + if (enable) + LOG_WARN("unimplemented: highlight mouse tracking"); + break; + + case 1002: + if (enable) + term->mouse_tracking = MOUSE_DRAG; + else if (term->mouse_tracking == MOUSE_DRAG) + term->mouse_tracking = MOUSE_NONE; + term_xcursor_update(term); + break; + + case 1003: + if (enable) + term->mouse_tracking = MOUSE_MOTION; + else if (term->mouse_tracking == MOUSE_MOTION) + term->mouse_tracking = MOUSE_NONE; + term_xcursor_update(term); + break; + + case 1004: + term->focus_events = enable; + if (enable) + term_to_slave(term, term->kbd_focus ? "\033[I" : "\033[O", 3); + break; + + case 1005: + if (enable) + LOG_WARN("unimplemented: mouse reporting mode: UTF-8"); +#if 0 + else if (term->mouse_reporting == MOUSE_UTF8) + term->mouse_reporting = MOUSE_NONE; +#endif + break; + + case 1006: + if (enable) + term->mouse_reporting = MOUSE_SGR; + else if (term->mouse_reporting == MOUSE_SGR) + term->mouse_reporting = MOUSE_NORMAL; + break; + + case 1007: + term->alt_scrolling = enable; + break; + + case 1015: + if (enable) + term->mouse_reporting = MOUSE_URXVT; + else if (term->mouse_reporting == MOUSE_URXVT) + term->mouse_reporting = MOUSE_NORMAL; + break; + + case 1016: + if (enable) + term->mouse_reporting = MOUSE_SGR_PIXELS; + else if (term->mouse_reporting == MOUSE_SGR_PIXELS) + term->mouse_reporting = MOUSE_NORMAL; + break; + + case 1034: + /* smm */ + LOG_DBG("%s 8-bit meta mode", enable ? "enabling" : "disabling"); + term->meta.eight_bit = enable; + break; + + case 1035: + /* numLock */ + LOG_DBG("%s Num Lock modifier", enable ? "enabling" : "disabling"); + term->num_lock_modifier = enable; + break; + + case 1036: + /* metaSendsEscape */ + LOG_DBG("%s meta-sends-escape", enable ? "enabling" : "disabling"); + term->meta.esc_prefix = enable; + break; + + case 1042: + term->bell_action_enabled = enable; + break; + +#if 0 + case 1043: + LOG_WARN("unimplemented: raise window on ctrl-g"); + break; +#endif + + case 1048: + if (enable) + term_save_cursor(term); + else + term_restore_cursor(term, &term->grid->saved_cursor); + break; + + case 47: + case 1047: + case 1049: + if (enable && term->grid != &term->alt) { + selection_cancel(term); + + if (param == 1049) + term_save_cursor(term); + + term->grid = &term->alt; + + /* Cursor retains its position from the normal grid */ + term_cursor_to( + term, + min(term->normal.cursor.point.row, term->rows - 1), + min(term->normal.cursor.point.col, term->cols - 1)); + + tll_free(term->normal.scroll_damage); + term_erase(term, 0, 0, term->rows - 1, term->cols - 1); + } + + else if (!enable && term->grid == &term->alt) { + selection_cancel(term); + + term->grid = &term->normal; + + /* Cursor retains its position from the alt grid */ + term_cursor_to( + term, min(term->alt.cursor.point.row, term->rows - 1), + min(term->alt.cursor.point.col, term->cols - 1)); + + if (param == 1049) + term_restore_cursor(term, &term->grid->saved_cursor); + + /* Delete all sixel images on the alt screen */ + tll_foreach(term->alt.sixel_images, it) { + sixel_destroy(&it->item); + tll_remove(term->alt.sixel_images, it); + } + + tll_free(term->alt.scroll_damage); + term_damage_view(term); + } + + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; + term_update_ascii_printer(term); + break; + + case 1070: + term->sixel.use_private_palette = enable; + break; + + case 2004: + term->bracketed_paste = enable; + break; + + case 2026: + if (enable) + term_enable_app_sync_updates(term); + else + term_disable_app_sync_updates(term); + break; + + case 2027: +#if defined(FOOT_GRAPHEME_CLUSTERING) + term->grapheme_shaping = enable; +#endif + break; + + case 2031: + term->report_theme_changes = enable; + break; + + case 2048: + if (enable) + term_enable_size_notifications(term); + else + term_disable_size_notifications(term); + break; + + case 8452: + term->sixel.cursor_right_of_graphics = enable; + break; + + case 737769: + if (enable) + term_ime_enable(term); + else { + term_ime_disable(term); + term->ime_reenable_after_url_mode = false; + } + break; + + default: + UNHANDLED(); + break; + } +} + +static void +decset(struct terminal *term, unsigned param) +{ + decset_decrst(term, param, true); +} + +static void +decrst(struct terminal *term, unsigned param) +{ + decset_decrst(term, param, false); +} + +/* + * These values represent the current state of a DEC private mode, + * as returned in the DECRPM reply to a DECRQM query. + */ +enum decrpm_status { + DECRPM_NOT_RECOGNIZED = 0, + DECRPM_SET = 1, + DECRPM_RESET = 2, + DECRPM_PERMANENTLY_SET = 3, + DECRPM_PERMANENTLY_RESET = 4, +}; + +static enum decrpm_status +decrpm(bool enabled) +{ + return enabled ? DECRPM_SET : DECRPM_RESET; +} + +static enum decrpm_status +decrqm(const struct terminal *term, unsigned param) +{ + switch (param) { + case 1: return decrpm(term->cursor_keys_mode == CURSOR_KEYS_APPLICATION); + case 5: return decrpm(term->reverse); + case 6: return decrpm(term->origin); + case 7: return decrpm(term->auto_margin); + case 9: return DECRPM_PERMANENTLY_RESET; /* term->mouse_tracking == MOUSE_X10; */ + case 12: return decrpm(term->cursor_blink.decset); + case 25: return decrpm(!term->hide_cursor); + case 45: return decrpm(term->reverse_wrap); + case 66: return decrpm(term->keypad_keys_mode == KEYPAD_APPLICATION); + case 67: return DECRPM_PERMANENTLY_RESET; /* https://vt100.net/docs/vt510-rm/DECBKM */ + case 80: return decrpm(!term->sixel.scrolling); + case 1000: return decrpm(term->mouse_tracking == MOUSE_CLICK); + case 1001: return DECRPM_PERMANENTLY_RESET; + case 1002: return decrpm(term->mouse_tracking == MOUSE_DRAG); + case 1003: return decrpm(term->mouse_tracking == MOUSE_MOTION); + case 1004: return decrpm(term->focus_events); + case 1005: return DECRPM_PERMANENTLY_RESET; /* term->mouse_reporting == MOUSE_UTF8; */ + case 1006: return decrpm(term->mouse_reporting == MOUSE_SGR); + case 1007: return decrpm(term->alt_scrolling); + case 1015: return decrpm(term->mouse_reporting == MOUSE_URXVT); + case 1016: return decrpm(term->mouse_reporting == MOUSE_SGR_PIXELS); + case 1034: return decrpm(term->meta.eight_bit); + case 1035: return decrpm(term->num_lock_modifier); + case 1036: return decrpm(term->meta.esc_prefix); + case 1042: return decrpm(term->bell_action_enabled); + case 47: /* FALLTHROUGH */ + case 1047: /* FALLTHROUGH */ + case 1049: return decrpm(term->grid == &term->alt); + case 1070: return decrpm(term->sixel.use_private_palette); + case 2004: return decrpm(term->bracketed_paste); + case 2026: return decrpm(term->render.app_sync_updates.enabled); + case 2027: return term->conf->tweak.grapheme_width_method != GRAPHEME_WIDTH_DOUBLE + ? DECRPM_PERMANENTLY_RESET + : decrpm(term->grapheme_shaping); + case 2031: return decrpm(term->report_theme_changes); + case 2048: return decrpm(term->size_notifications); + case 8452: return decrpm(term->sixel.cursor_right_of_graphics); + case 737769: return decrpm(term_ime_is_enabled(term)); + } + + return DECRPM_NOT_RECOGNIZED; +} + +static void +xtsave(struct terminal *term, unsigned param) +{ + switch (param) { + case 1: term->xtsave.application_cursor_keys = term->cursor_keys_mode == CURSOR_KEYS_APPLICATION; break; + case 5: term->xtsave.reverse = term->reverse; break; + case 6: term->xtsave.origin = term->origin; break; + case 7: term->xtsave.auto_margin = term->auto_margin; break; + case 9: /* term->xtsave.mouse_x10 = term->mouse_tracking == MOUSE_X10; */ break; + case 12: term->xtsave.cursor_blink = term->cursor_blink.decset; break; + case 25: term->xtsave.show_cursor = !term->hide_cursor; break; + case 45: term->xtsave.reverse_wrap = term->reverse_wrap; break; + case 47: term->xtsave.alt_screen = term->grid == &term->alt; break; + case 66: term->xtsave.application_keypad_keys = term->keypad_keys_mode == KEYPAD_APPLICATION; break; + case 67: break; + case 80: term->xtsave.sixel_display_mode = !term->sixel.scrolling; break; + case 1000: term->xtsave.mouse_click = term->mouse_tracking == MOUSE_CLICK; break; + case 1001: break; + case 1002: term->xtsave.mouse_drag = term->mouse_tracking == MOUSE_DRAG; break; + case 1003: term->xtsave.mouse_motion = term->mouse_tracking == MOUSE_MOTION; break; + case 1004: term->xtsave.focus_events = term->focus_events; break; + case 1005: /* term->xtsave.mouse_utf8 = term->mouse_reporting == MOUSE_UTF8; */ break; + case 1006: term->xtsave.mouse_sgr = term->mouse_reporting == MOUSE_SGR; break; + case 1007: term->xtsave.alt_scrolling = term->alt_scrolling; break; + case 1015: term->xtsave.mouse_urxvt = term->mouse_reporting == MOUSE_URXVT; break; + case 1016: term->xtsave.mouse_sgr_pixels = term->mouse_reporting == MOUSE_SGR_PIXELS; break; + case 1034: term->xtsave.meta_eight_bit = term->meta.eight_bit; break; + case 1035: term->xtsave.num_lock_modifier = term->num_lock_modifier; break; + case 1036: term->xtsave.meta_esc_prefix = term->meta.esc_prefix; break; + case 1042: term->xtsave.bell_action_enabled = term->bell_action_enabled; break; + case 1047: term->xtsave.alt_screen = term->grid == &term->alt; break; + case 1048: term_save_cursor(term); break; + case 1049: term->xtsave.alt_screen = term->grid == &term->alt; break; + case 1070: term->xtsave.sixel_private_palette = term->sixel.use_private_palette; break; + case 2004: term->xtsave.bracketed_paste = term->bracketed_paste; break; + case 2026: term->xtsave.app_sync_updates = term->render.app_sync_updates.enabled; break; + case 2027: term->xtsave.grapheme_shaping = term->grapheme_shaping; break; + case 2031: term->xtsave.report_theme_changes = term->report_theme_changes; break; + case 2048: term->xtsave.size_notifications = term->size_notifications; break; + case 8452: term->xtsave.sixel_cursor_right_of_graphics = term->sixel.cursor_right_of_graphics; break; + case 737769: term->xtsave.ime = term_ime_is_enabled(term); break; + } +} + +static void +xtrestore(struct terminal *term, unsigned param) +{ + bool enable; + switch (param) { + case 1: enable = term->xtsave.application_cursor_keys; break; + case 5: enable = term->xtsave.reverse; break; + case 6: enable = term->xtsave.origin; break; + case 7: enable = term->xtsave.auto_margin; break; + case 9: /* enable = term->xtsave.mouse_x10; break; */ return; + case 12: enable = term->xtsave.cursor_blink; break; + case 25: enable = term->xtsave.show_cursor; break; + case 45: enable = term->xtsave.reverse_wrap; break; + case 47: enable = term->xtsave.alt_screen; break; + case 66: enable = term->xtsave.application_keypad_keys; break; + case 67: return; + case 80: enable = term->xtsave.sixel_display_mode; break; + case 1000: enable = term->xtsave.mouse_click; break; + case 1001: return; + case 1002: enable = term->xtsave.mouse_drag; break; + case 1003: enable = term->xtsave.mouse_motion; break; + case 1004: enable = term->xtsave.focus_events; break; + case 1005: /* enable = term->xtsave.mouse_utf8; break; */ return; + case 1006: enable = term->xtsave.mouse_sgr; break; + case 1007: enable = term->xtsave.alt_scrolling; break; + case 1015: enable = term->xtsave.mouse_urxvt; break; + case 1016: enable = term->xtsave.mouse_sgr_pixels; break; + case 1034: enable = term->xtsave.meta_eight_bit; break; + case 1035: enable = term->xtsave.num_lock_modifier; break; + case 1036: enable = term->xtsave.meta_esc_prefix; break; + case 1042: enable = term->xtsave.bell_action_enabled; break; + case 1047: enable = term->xtsave.alt_screen; break; + case 1048: enable = true; break; + case 1049: enable = term->xtsave.alt_screen; break; + case 1070: enable = term->xtsave.sixel_private_palette; break; + case 2004: enable = term->xtsave.bracketed_paste; break; + case 2026: enable = term->xtsave.app_sync_updates; break; + case 2027: enable = term->xtsave.grapheme_shaping; break; + case 2031: enable = term->xtsave.report_theme_changes; break; + case 2048: enable = term->xtsave.size_notifications; break; + case 8452: enable = term->xtsave.sixel_cursor_right_of_graphics; break; + case 737769: enable = term->xtsave.ime; break; + + default: return; + } + + decset_decrst(term, param, enable); +} + +static bool +params_to_rectangular_area(const struct terminal *term, int first_idx, + int *top, int *left, int *bottom, int *right) +{ + int rel_top = vt_param_get(term, first_idx + 0, 1) - 1; + *left = min(vt_param_get(term, first_idx + 1, 1) - 1, term->cols - 1); + int rel_bottom = vt_param_get(term, first_idx + 2, term->rows) - 1; + *right = min(vt_param_get(term, first_idx + 3, term->cols) - 1, term->cols - 1); + + if (rel_top > rel_bottom || *left > *right) + return false; + + *top = term_row_rel_to_abs(term, rel_top); + *bottom = term_row_rel_to_abs(term, rel_bottom); + + return true; +} + +void +csi_dispatch(struct terminal *term, uint8_t final) +{ + LOG_DBG("%s (%08x)", csi_as_string(term, final, -1), term->vt.private); + + switch (term->vt.private) { + case 0: { + switch (final) { + case 'b': + if (term->vt.last_printed != 0) { + /* + * Note: we never reset 'last-printed'. According to + * ECMA-48, the behaviour is undefined if REP was + * _not_ preceded by a graphical character. + */ + int count = vt_param_get(term, 0, 1); + LOG_DBG("REP: '%lc' %d times", (wint_t)term->vt.last_printed, count); + + int width; + + if (term->vt.last_printed >= CELL_COMB_CHARS_LO) { + const struct composed *comp = composed_lookup( + term->composed, term->vt.last_printed - CELL_COMB_CHARS_LO); + + xassert(comp != NULL); + width = comp->forced_width > 0 ? comp->forced_width : comp->width; + } else + width = c32width(term->vt.last_printed); + + if (width > 0) { + for (int i = 0; i < count; i++) + term_print(term, term->vt.last_printed, width, false); + } + } + break; + + case 'c': { + if (vt_param_get(term, 0, 0) != 0) { + UNHANDLED(); + break; + } + + /* Send Device Attributes (Primary DA) */ + + /* + * Responses: + * - CSI?1;2c vt100 with advanced video option + * - CSI?1;0c vt101 with no options + * - CSI?6c vt102 + * - CSI?62;c vt220 + * - CSI?63;c vt320 + * - CSI?64;c vt420 + * + * Ps (response may contain multiple): + * - 1 132 columns + * - 2 Printer. + * - 3 ReGIS graphics. + * - 4 Sixel graphics. + * - 6 Selective erase. + * - 8 User-defined keys. + * - 9 National Replacement Character sets. + * - 15 Technical characters. + * - 16 Locator port. + * - 17 Terminal state interrogation. + * - 18 User windows. + * - 21 Horizontal scrolling. + * - 22 ANSI color, e.g., VT525. + * - 28 Rectangular editing. + * - 29 ANSI text locator (i.e., DEC Locator mode). + * - 52 Clipboard access + * + * Note: we report ourselves as a VT220, mainly to be able + * to pass parameters, to indicate we support sixel, and + * ANSI colors. + * + * The VT level must be synchronized with the secondary DA + * response. + * + * Note: tertiary DA responds with "FOOT". + */ + char reply[32]; + + int len = snprintf( + reply, sizeof(reply), "\033[?62%s;22;28%sc", + term->conf->tweak.sixel ? ";4" : "", + (term->conf->security.osc52 == OSC52_ENABLED || + term->conf->security.osc52 == OSC52_COPY_ENABLED ? ";52" : "")); + + term_to_slave(term, reply, len); + break; + } + + case 'd': { + /* VPA - vertical line position absolute */ + int rel_row = vt_param_get(term, 0, 1) - 1; + int row = term_row_rel_to_abs(term, rel_row); + term_cursor_to(term, row, term->grid->cursor.point.col); + break; + } + + case 'm': + csi_sgr(term); + break; + + case 'A': + term_cursor_up(term, vt_param_get(term, 0, 1)); + break; + + case 'e': + case 'B': + term_cursor_down(term, vt_param_get(term, 0, 1)); + break; + + case 'a': + case 'C': + term_cursor_right(term, vt_param_get(term, 0, 1)); + break; + + case 'D': + term_cursor_left(term, vt_param_get(term, 0, 1)); + break; + + case 'E': + /* CNL - Cursor Next Line */ + term_cursor_down(term, vt_param_get(term, 0, 1)); + term_cursor_left(term, term->grid->cursor.point.col); + break; + + case 'F': + /* CPL - Cursor Previous Line */ + term_cursor_up(term, vt_param_get(term, 0, 1)); + term_cursor_left(term, term->grid->cursor.point.col); + break; + + case 'g': { + int param = vt_param_get(term, 0, 0); + switch (param) { + case 0: + /* Clear tab stop at *current* column */ + tll_foreach(term->tab_stops, it) { + if (it->item == term->grid->cursor.point.col) + tll_remove(term->tab_stops, it); + else if (it->item > term->grid->cursor.point.col) + break; + } + + break; + + case 3: + /* Clear *all* tabs */ + tll_free(term->tab_stops); + break; + + default: + UNHANDLED(); + break; + } + break; + } + + case '`': + case 'G': { + /* Cursor horizontal absolute */ + int col = min(vt_param_get(term, 0, 1), term->cols) - 1; + term_cursor_col(term, col); + break; + } + + case 'f': + case 'H': { + /* Move cursor */ + int rel_row = vt_param_get(term, 0, 1) - 1; + int row = term_row_rel_to_abs(term, rel_row); + int col = min(vt_param_get(term, 1, 1), term->cols) - 1; + term_cursor_to(term, row, col); + break; + } + + case 'J': { + /* Erase screen */ + + int param = vt_param_get(term, 0, 0); + switch (param) { + case 0: { + /* From cursor to end of screen */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase( + term, + cursor->row, cursor->col, + term->rows - 1, term->cols - 1); + term->grid->cursor.lcf = false; + break; + } + + case 1: { + /* From start of screen to cursor */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase(term, 0, 0, cursor->row, cursor->col); + term->grid->cursor.lcf = false; + break; + } + + case 2: + /* Erase entire screen */ + term_erase(term, 0, 0, term->rows - 1, term->cols - 1); + term->grid->cursor.lcf = false; + break; + + case 3: { + /* Erase scrollback */ + term_erase_scrollback(term); + break; + } + + default: + UNHANDLED(); + break; + } + break; + } + + case 'K': { + /* Erase line */ + + int param = vt_param_get(term, 0, 0); + switch (param) { + case 0: { + /* From cursor to end of line */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase( + term, + cursor->row, cursor->col, + cursor->row, term->cols - 1); + term->grid->cursor.lcf = false; + break; + } + + case 1: { + /* From start of line to cursor */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase(term, cursor->row, 0, cursor->row, cursor->col); + term->grid->cursor.lcf = false; + break; + } + + case 2: { + /* Entire line */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase(term, cursor->row, 0, cursor->row, term->cols - 1); + term->grid->cursor.lcf = false; + break; + } + + default: + UNHANDLED(); + break; + } + + break; + } + + case 'L': { /* IL */ + if (term->grid->cursor.point.row < term->scroll_region.start || + term->grid->cursor.point.row >= term->scroll_region.end) + break; + + int count = min( + vt_param_get(term, 0, 1), + term->scroll_region.end - term->grid->cursor.point.row); + + term_scroll_reverse_partial( + term, + (struct scroll_region){ + .start = term->grid->cursor.point.row, + .end = term->scroll_region.end}, + count); + term->grid->cursor.lcf = false; + term->grid->cursor.point.col = 0; + break; + } + + case 'M': { /* DL */ + if (term->grid->cursor.point.row < term->scroll_region.start || + term->grid->cursor.point.row >= term->scroll_region.end) + break; + + int count = min( + vt_param_get(term, 0, 1), + term->scroll_region.end - term->grid->cursor.point.row); + + term_scroll_partial( + term, + (struct scroll_region){ + .start = term->grid->cursor.point.row, + .end = term->scroll_region.end}, + count); + term->grid->cursor.lcf = false; + term->grid->cursor.point.col = 0; + break; + } + + case 'P': { + /* DCH: Delete character(s) */ + + /* Number of characters to delete */ + int count = min( + vt_param_get(term, 0, 1), term->cols - term->grid->cursor.point.col); + + /* Number of characters left after deletion (on current line) */ + int remaining = term->cols - (term->grid->cursor.point.col + count); + + /* 'Delete' characters by moving the remaining ones */ + memmove(&term->grid->cur_row->cells[term->grid->cursor.point.col], + &term->grid->cur_row->cells[term->grid->cursor.point.col + count], + remaining * sizeof(term->grid->cur_row->cells[0])); + + for (size_t c = 0; c < remaining; c++) + term->grid->cur_row->cells[term->grid->cursor.point.col + c].attrs.clean = 0; + term->grid->cur_row->dirty = true; + + /* Erase the remainder of the line */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase( + term, + cursor->row, cursor->col + remaining, + cursor->row, term->cols - 1); + term->grid->cursor.lcf = false; + break; + } + + case '@': { + /* ICH: insert character(s) */ + + /* Number of characters to insert */ + int count = min( + vt_param_get(term, 0, 1), term->cols - term->grid->cursor.point.col); + + /* Characters to move */ + int remaining = term->cols - (term->grid->cursor.point.col + count); + + /* Push existing characters */ + memmove(&term->grid->cur_row->cells[term->grid->cursor.point.col + count], + &term->grid->cur_row->cells[term->grid->cursor.point.col], + remaining * sizeof(term->grid->cur_row->cells[0])); + for (size_t c = 0; c < remaining; c++) + term->grid->cur_row->cells[term->grid->cursor.point.col + count + c].attrs.clean = 0; + term->grid->cur_row->dirty = true; + + /* Erase (insert space characters) */ + const struct coord *cursor = &term->grid->cursor.point; + term_erase( + term, + cursor->row, cursor->col, + cursor->row, cursor->col + count - 1); + term->grid->cursor.lcf = false; + break; + } + + case 'S': { + const struct scroll_region *r = &term->scroll_region; + int amount = min(vt_param_get(term, 0, 1), r->end - r->start); + term_scroll(term, amount); + break; + } + + case 'T': { + const struct scroll_region *r = &term->scroll_region; + int amount = min(vt_param_get(term, 0, 1), r->end - r->start); + term_scroll_reverse(term, amount); + break; + } + + case 'X': { + /* Erase chars */ + int count = min( + vt_param_get(term, 0, 1), term->cols - term->grid->cursor.point.col); + + const struct coord *cursor = &term->grid->cursor.point; + term_erase( + term, + cursor->row, cursor->col, + cursor->row, cursor->col + count - 1); + term->grid->cursor.lcf = false; + break; + } + + case 'I': { + /* CHT - Tab Forward (param is number of tab stops to move through) */ + for (int i = 0; i < vt_param_get(term, 0, 1); i++) { + int new_col = term->cols - 1; + tll_foreach(term->tab_stops, it) { + if (it->item > term->grid->cursor.point.col) { + new_col = it->item; + break; + } + } + xassert(new_col >= term->grid->cursor.point.col); + + bool lcf = term->grid->cursor.lcf; + term_cursor_right(term, new_col - term->grid->cursor.point.col); + term->grid->cursor.lcf = lcf; + } + break; + } + + case 'Z': + /* CBT - Back tab (param is number of tab stops to move back through) */ + for (int i = 0; i < vt_param_get(term, 0, 1); i++) { + int new_col = 0; + tll_rforeach(term->tab_stops, it) { + if (it->item < term->grid->cursor.point.col) { + new_col = it->item; + break; + } + } + xassert(term->grid->cursor.point.col >= new_col); + term_cursor_left(term, term->grid->cursor.point.col - new_col); + } + break; + + case 'h': + case 'l': { + /* Set/Reset Mode (SM/RM) */ + int param = vt_param_get(term, 0, 0); + bool sm = final == 'h'; + if (param == 4) { + /* Insertion Replacement Mode (IRM) */ + term->insert_mode = sm; + term->bits_affecting_ascii_printer.insert_mode = sm; + term_update_ascii_printer(term); + break; + } + + /* + * ECMA-48 defines modes 1-22, all of which were optional + * (§7.1; "may have one state only") and are considered + * deprecated (§7.1) in the latest (5th) edition. xterm only + * documents modes 2, 4, 12 and 20, the last of which was + * outright removed (§8.3.106) in 5th edition ECMA-48. + */ + if (sm) { + LOG_WARN("SM with unimplemented mode: %d", param); + } + break; + } + + case 'r': { + int start = vt_param_get(term, 0, 1); + int end = min(vt_param_get(term, 1, term->rows), term->rows); + + if (end > start) { + + /* 1-based */ + term->scroll_region.start = start - 1; + term->scroll_region.end = end; + term_cursor_home(term); + + LOG_DBG("scroll region: %d-%d", + term->scroll_region.start, + term->scroll_region.end); + } + break; + } + + case 's': + term_save_cursor(term); + break; + + case 'u': + term_restore_cursor(term, &term->grid->saved_cursor); + break; + + case 't': { + /* + * Window operations + */ + + const unsigned param = vt_param_get(term, 0, 0); + + switch (param) { + case 1: LOG_WARN("unimplemented: de-iconify"); break; + case 2: LOG_WARN("unimplemented: iconify"); break; + case 3: LOG_WARN("unimplemented: move window to pixel position"); break; + case 4: LOG_WARN("unimplemented: resize window in pixels"); break; + case 5: LOG_WARN("unimplemented: raise window to front of stack"); break; + case 6: LOG_WARN("unimplemented: raise window to back of stack"); break; + case 7: LOG_WARN("unimplemented: refresh window"); break; + case 8: LOG_WARN("unimplemented: resize window in chars"); break; + case 9: LOG_WARN("unimplemented: maximize/unmaximize window"); break; + case 10: LOG_WARN("unimplemented: to/from full screen"); break; + case 20: LOG_WARN("unimplemented: report icon label"); break; + case 24: LOG_WARN("unimplemented: resize window (DECSLPP)"); break; + + case 11: /* report if window is iconified */ + /* We don't know - always report *not* iconified */ + /* 1=not iconified, 2=iconified */ + term_to_slave(term, "\033[1t", 4); + break; + + case 13: { /* report window position */ + /* We don't know our position - always report (0,0) */ + static const char reply[] = "\033[3;0;0t"; + switch (vt_param_get(term, 1, 0)) { + case 0: /* window position */ + case 2: /* text area position */ + term_to_slave(term, reply, sizeof(reply) - 1); + break; + + default: + UNHANDLED(); + break; + } + + break; + } + + case 14: { /* report window size in pixels */ + int width = -1; + int height = -1; + + switch (vt_param_get(term, 1, 0)) { + case 0: + /* text area size */ + width = term->width - term->margins.left - term->margins.right; + height = term->height - term->margins.top - term->margins.bottom; + break; + + case 2: + /* window size */ + width = term->width; + height = term->height; + break; + + default: + UNHANDLED(); + break; + } + + if (width >= 0 && height >= 0) { + char reply[64]; + size_t n = xsnprintf( + reply, sizeof(reply), "\033[4;%d;%dt", height, width); + term_to_slave(term, reply, n); + } + break; + } + + case 15: /* report screen size in pixels */ + tll_foreach(term->window->on_outputs, it) { + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[5;%d;%dt", + it->item->dim.px_real.height, + it->item->dim.px_real.width); + term_to_slave(term, reply, n); + break; + } + + if (tll_length(term->window->on_outputs) == 0) + term_to_slave(term, "\033[5;0;0t", 8); + break; + + case 16: { /* report cell size in pixels */ + char reply[64]; + size_t n = xsnprintf( + reply, sizeof(reply), "\033[6;%d;%dt", + term->cell_height, term->cell_width); + term_to_slave(term, reply, n); + break; + } + + case 18: { /* text area size in chars */ + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[8;%d;%dt", + term->rows, term->cols); + term_to_slave(term, reply, n); + break; + } + + case 19: { /* report screen size in chars */ + tll_foreach(term->window->on_outputs, it) { + char reply[64]; + size_t n = xsnprintf( + reply, sizeof(reply), "\033[9;%d;%dt", + it->item->dim.px_real.height / term->cell_height, + it->item->dim.px_real.width / term->cell_width); + term_to_slave(term, reply, n); + break; + } + + if (tll_length(term->window->on_outputs) == 0) + term_to_slave(term, "\033[9;0;0t", 8); + break; + } + + case 21: { +#if 0 /* Disabled for now, see #1894 */ + char reply[3 + strlen(term->window_title) + 2 + 1]; + int chars = xsnprintf( + reply, sizeof(reply), "\033]l%s\033\\", term->window_title); + term_to_slave(term, reply, chars); +#else + LOG_WARN("CSI 21 t (report window title) ignored"); +#endif + break; + } + + case 22: { /* push window title */ + /* 0 - icon + title, 1 - icon, 2 - title */ + unsigned what = vt_param_get(term, 1, 0); + if (what == 0 || what == 2) { + tll_push_back( + term->window_title_stack, xstrdup(term->window_title)); + } + break; + } + + case 23: { /* pop window title */ + /* 0 - icon + title, 1 - icon, 2 - title */ + unsigned what = vt_param_get(term, 1, 0); + if (what == 0 || what == 2) { + if (tll_length(term->window_title_stack) > 0) { + char *title = tll_pop_back(term->window_title_stack); + term_set_window_title(term, title); + free(title); + } + } + break; + } + + case 1001: { + } + + default: + LOG_DBG("ignoring %s", csi_as_string(term, final, -1)); + break; + } + break; + } + + case 'n': { + if (term->vt.params.idx > 0) { + int param = vt_param_get(term, 0, 0); + switch (param) { + case 5: + /* Query device status */ + term_to_slave(term, "\x1b[0n", 4); /* "Device OK" */ + break; + + case 6: { + /* u7 - cursor position query */ + + int row = term->origin == ORIGIN_ABSOLUTE + ? term->grid->cursor.point.row + : term->grid->cursor.point.row - term->scroll_region.start; + + /* TODO: we use 0-based position, while the xterm + * terminfo says the receiver of the reply should + * decrement, hence we must add 1 */ + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\x1b[%d;%dR", + row + 1, term->grid->cursor.point.col + 1); + term_to_slave(term, reply, n); + break; + } + + default: + UNHANDLED(); + break; + } + } else + UNHANDLED(); + + break; + } + + default: + UNHANDLED(); + break; + } + + break; /* private[0] == 0 */ + } + + case '?': { + switch (final) { + case 'h': + /* DECSET - DEC private mode set */ + for (size_t i = 0; i < term->vt.params.idx; i++) + decset(term, term->vt.params.v[i].value); + break; + + case 'l': + /* DECRST - DEC private mode reset */ + for (size_t i = 0; i < term->vt.params.idx; i++) + decrst(term, term->vt.params.v[i].value); + break; + + case 's': + for (size_t i = 0; i < term->vt.params.idx; i++) + xtsave(term, term->vt.params.v[i].value); + break; + + case 'r': + for (size_t i = 0; i < term->vt.params.idx; i++) + xtrestore(term, term->vt.params.v[i].value); + break; + + case 'S': { + if (!term->conf->tweak.sixel) { + UNHANDLED(); + break; + } + + unsigned target = vt_param_get(term, 0, 0); + unsigned operation = vt_param_get(term, 1, 0); + + switch (target) { + case 1: + switch (operation) { + case 1: sixel_colors_report_current(term); break; + case 2: sixel_colors_reset(term); break; + case 3: sixel_colors_set(term, vt_param_get(term, 2, 0)); break; + case 4: sixel_colors_report_max(term); break; + default: UNHANDLED(); break; + } + break; + + case 2: + switch (operation) { + case 1: sixel_geometry_report_current(term); break; + case 2: sixel_geometry_reset(term); break; + case 3: sixel_geometry_set(term, vt_param_get(term, 2, 0), vt_param_get(term, 3, 0)); break; + case 4: sixel_geometry_report_max(term); break; + default: UNHANDLED(); break; + } + break; + + default: + UNHANDLED(); + break; + } + + break; + } + + case 'm': { + int resource = vt_param_get(term, 0, 0); + int value = -1; + + switch (resource) { + case 0: /* modifyKeyboard */ + value = 0; + break; + + case 1: /* modifyCursorKeys */ + case 2: /* modifyFunctionKeys */ + value = 1; + break; + + case 4: /* modifyOtherKeys */ + value = term->modify_other_keys_2 ? 2 : 1; + break; + + default: + LOG_WARN("XTQMODKEYS: invalid resource '%d' in '%s'", + resource, csi_as_string(term, final, -1)); + break; + } + + if (value >= 0) { + char reply[16] = {0}; + int chars = snprintf(reply, sizeof(reply), + "\033[>%d;%dm", resource, value); + term_to_slave(term, reply, chars); + } + break; + } + + case 'n': { + const int param = vt_param_get(term, 0, 0); + + switch (param) { + case 996: { /* Query current theme mode (see private mode 2031) */ + /* + * 1 - dark mode + * 2 - light mode + * + * In foot, the themes aren't necessarily light/dark, + * but by convention, the primary theme is dark, and + * the alternative theme is light. + */ + char reply[16] = {0}; + int chars = snprintf( + reply, sizeof(reply), + "\033[?997;%dn", + term->colors.active_theme == COLOR_THEME_DARK ? 1 : 2); + + term_to_slave(term, reply, chars); + break; + } + } + break; + } + + case 'p': { + /* + * Request status of ECMA-48/"ANSI" private mode (DECRQM + * for SM/RM modes; see private="?$" case further below for + * DECSET/DECRST modes) + */ + unsigned param = vt_param_get(term, 0, 0); + unsigned status = DECRPM_NOT_RECOGNIZED; + if (param == 4) { + status = decrpm(term->insert_mode); + } + char reply[32]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[%u;%u$y", param, status); + term_to_slave(term, reply, n); + break; + } + + case 'u': { + enum kitty_kbd_flags flags = + term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx]; + + char reply[8]; + int chars = snprintf(reply, sizeof(reply), "\033[?%uu", flags); + term_to_slave(term, reply, chars); + break; + } + + default: + UNHANDLED(); + break; + } + + break; /* private[0] == '?' */ + } + + case '>': { + switch (final) { + case 'c': + /* Send Device Attributes (Secondary DA) */ + if (vt_param_get(term, 0, 0) != 0) { + UNHANDLED(); + break; + } + + /* + * Param 1 - terminal type: + * 0 - vt100 + * 1 - vt220 + * 2 - vt240 + * 18 - vt330 + * 19 - vt340 + * 24 - vt320 + * 41 - vt420 + * 61 - vt510 + * 64 - vt520 + * 65 - vt525 + * + * Param 2 - firmware version xterm uses its version + * number. We do to, in the format "MAJORMINORPATCH", + * where all three version numbers are always two + * digits. So e.g. 1.25.0 is reported as 012500. + * + * We report ourselves as a VT220. This must be + * synchronized with the primary DA response. + * + * Note: tertiary DA replies with "FOOT". + */ + + static_assert(FOOT_MAJOR < 100, "Major version must not exceed 99"); + static_assert(FOOT_MINOR < 100, "Minor version must not exceed 99"); + static_assert(FOOT_PATCH < 100, "Patch version must not exceed 99"); + + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[>1;%02u%02u%02u;0c", + FOOT_MAJOR, FOOT_MINOR, FOOT_PATCH); + + term_to_slave(term, reply, n); + break; + + case 'm': + if (term->vt.params.idx == 0) { + /* Reset all */ + } else { + int resource = vt_param_get(term, 0, 0); + int value = vt_param_get(term, 1, -1); + + switch (resource) { + case 0: /* modifyKeyboard */ + break; + + case 1: /* modifyCursorKeys */ + case 2: /* modifyFunctionKeys */ + /* Ignored, we always report modifiers */ + if (value != 2 && value != -1) { + LOG_WARN( + "unimplemented: %s = %d", + resource == 1 ? "modifyCursorKeys" : + resource == 2 ? "modifyFunctionKeys" : "", + value); + } + break; + + case 4: /* modifyOtherKeys */ + term->modify_other_keys_2 = value == 2; + LOG_DBG("modifyOtherKeys=%d", value); + break; + + default: + LOG_WARN("XTMODKEYS: invalid resource '%d' in '%s'", + resource, csi_as_string(term, final, -1)); + break; + } + } + break; /* final == 'm' */ + + case 'n': { + int resource = vt_param_get(term, 0, 2); /* Default is modifyFunctionKeys */ + switch (resource) { + case 0: /* modifyKeyboard */ + case 1: /* modifyCursorKeys */ + case 2: /* modifyFunctionKeys */ + break; + + case 4: /* modifyOtherKeys */ + /* We don't support fully disabling modifyOtherKeys, + * but simply revert back to mode '1' */ + term->modify_other_keys_2 = false; + LOG_DBG("modifyOtherKeys=1"); + break; + } + break; + } + + case 'u': { + int flags = vt_param_get(term, 0, 0) & KITTY_KBD_SUPPORTED; + + struct grid *grid = term->grid; + uint8_t idx = grid->kitty_kbd.idx; + + if (idx + 1 >= ALEN(grid->kitty_kbd.flags)) { + /* Stack full, evict oldest by wrapping around */ + idx = 0; + } else + idx++; + + grid->kitty_kbd.flags[idx] = flags; + grid->kitty_kbd.idx = idx; + + LOG_DBG("kitty kbd: pushed new flags: 0x%03x", flags); + break; + } + + case 'q': { + /* XTVERSION */ + if (vt_param_get(term, 0, 0) != 0) { + UNHANDLED(); + break; + } + + char reply[64]; + size_t n = xsnprintf( + reply, sizeof(reply), "\033P>|foot(%u.%u.%u%s%s)\033\\", + FOOT_MAJOR, FOOT_MINOR, FOOT_PATCH, + FOOT_EXTRA[0] != '\0' ? "-" : "", FOOT_EXTRA); + term_to_slave(term, reply, n); + break; + } + + default: + UNHANDLED(); + break; + } + + break; /* private[0] == '>' */ + } + + case '<': { + switch (final) { + case 'u': { + int count = vt_param_get(term, 0, 1); + LOG_DBG("kitty kbd: popping %d levels of flags", count); + + struct grid *grid = term->grid; + uint8_t idx = grid->kitty_kbd.idx; + + for (int i = 0; i < count; i++) { + /* Reset flags. This ensures we get flags=0 when + * over-popping */ + grid->kitty_kbd.flags[idx] = 0; + + if (idx == 0) + idx = ALEN(grid->kitty_kbd.flags) - 1; + else + idx--; + } + + grid->kitty_kbd.idx = idx; + + LOG_DBG("kitty kbd: flags after pop: 0x%03x", + term->grid->kitty_kbd.flags[idx]); + break; + } + } + break; /* private[0] == '<' */ + } + + case ' ': { + switch (final) { + case 'q': { + int param = vt_param_get(term, 0, 0); + switch (param) { + case 0: /* blinking block, but we use it to reset to configured default */ + term->cursor_style = term->conf->cursor.style; + term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; + term_cursor_blink_update(term); + break; + + case 1: /* blinking block */ + case 2: /* steady block */ + term->cursor_style = term->conf->cursor.style == CURSOR_HOLLOW + ? CURSOR_HOLLOW : CURSOR_BLOCK; + break; + + case 3: /* blinking underline */ + case 4: /* steady underline */ + term->cursor_style = CURSOR_UNDERLINE; + break; + + case 5: /* blinking bar */ + case 6: /* steady bar */ + term->cursor_style = CURSOR_BEAM; + break; + + default: + UNHANDLED(); + break; + } + + if (param > 0 && param <= 6) { + term->cursor_blink.deccsusr = param & 1; + term_cursor_blink_update(term); + } + break; + } + + default: + UNHANDLED(); + break; + } + break; /* private[0] == ' ' */ + } + + case '!': { + if (final == 'p') { + term_reset(term, false); + break; + } + + UNHANDLED(); + break; /* private[0] == '!' */ + } + + case '=': { + switch (final) { + case 'c': + if (vt_param_get(term, 0, 0) != 0) { + UNHANDLED(); + break; + } + + /* + * Send Device Attributes (Tertiary DA) + * + * Reply format is "DCS ! | DDDDDDDD ST" + * + * D..D is the unit ID of the terminal, consisting of four + * hexadecimal pairs. The first pair represents the + * manufacturing site code. This code can be any + * hexadecimal value from 00 through FF. + */ + + term_to_slave(term, "\033P!|464f4f54\033\\", 14); /* FOOT */ + break; + + case 'u': { + int flag_set = vt_param_get(term, 0, 0) & KITTY_KBD_SUPPORTED; + int mode = vt_param_get(term, 1, 1); + + struct grid *grid = term->grid; + uint8_t idx = grid->kitty_kbd.idx; + + switch (mode) { + case 1: + /* set bits are set, unset bits are reset */ + grid->kitty_kbd.flags[idx] = flag_set; + break; + + case 2: + /* set bits are set, unset bits are left unchanged */ + grid->kitty_kbd.flags[idx] |= flag_set; + break; + + case 3: + /* set bits are reset, unset bits are left unchanged */ + grid->kitty_kbd.flags[idx] &= ~flag_set; + break; + + default: + UNHANDLED(); + break; + } + + LOG_DBG("kitty kbd: flags after update: 0x%03x", + grid->kitty_kbd.flags[idx]); + break; + } + + default: + UNHANDLED(); + break; + } + break; /* private[0] == '=' */ + } + + case '$': { + switch (final) { + case 'r': { /* DECCARA */ + int top, left, bottom, right; + if (!params_to_rectangular_area( + term, 0, &top, &left, &bottom, &right)) + { + break; + } + + for (int r = top; r <= bottom; r++) { + struct row *row = grid_row(term->grid, r); + row->dirty = true; + + for (int c = left; c <= right; c++) { + struct attributes *a = &row->cells[c].attrs; + a->clean = 0; + + for (size_t i = 4; i < term->vt.params.idx; i++) { + const int param = term->vt.params.v[i].value; + + /* DECCARA only supports a sub-set of SGR parameters */ + switch (param) { + case 0: + a->bold = false; + a->underline = false; + a->blink = false; + a->reverse = false; + break; + + case 1: a->bold = true; break; + case 4: a->underline = true; break; + case 5: a->blink = true; break; + case 7: a->reverse = true; break; + + case 22: a->bold = false; break; + case 24: a->underline = false; break; + case 25: a->blink = false; break; + case 27: a->reverse = false; break; + } + } + } + } + break; + } + + case 't': { /* DECRARA */ + int top, left, bottom, right; + if (!params_to_rectangular_area( + term, 0, &top, &left, &bottom, &right)) + { + break; + } + + for (int r = top; r <= bottom; r++) { + struct row *row = grid_row(term->grid, r); + row->dirty = true; + + for (int c = left; c <= right; c++) { + struct attributes *a = &row->cells[c].attrs; + a->clean = 0; + + for (size_t i = 4; i < term->vt.params.idx; i++) { + const int param = term->vt.params.v[i].value; + + /* DECRARA only supports a sub-set of SGR parameters */ + switch (param) { + case 0: + a->bold = !a->bold; + a->underline = !a->underline; + a->blink = !a->blink; + a->reverse = !a->reverse; + break; + + case 1: a->bold = !a->bold; break; + case 4: a->underline = !a->underline; break; + case 5: a->blink = !a->blink; break; + case 7: a->reverse = !a->reverse; break; + } + } + } + } + break; + } + + case 'v': { /* DECCRA */ + int src_top, src_left, src_bottom, src_right; + if (!params_to_rectangular_area( + term, 0, &src_top, &src_left, &src_bottom, &src_right)) + { + break; + } + + int src_page = vt_param_get(term, 4, 1); + + int dst_rel_top = vt_param_get(term, 5, 1) - 1; + int dst_left = vt_param_get(term, 6, 1) - 1; + int dst_page = vt_param_get(term, 7, 1); + + if (unlikely(src_page != 1 || dst_page != 1)) { + /* We don’t support “pages” */ + break; + } + + int dst_rel_bottom = dst_rel_top + (src_bottom - src_top); + int dst_right = min(dst_left + (src_right - src_left), term->cols - 1); + + int dst_top = term_row_rel_to_abs(term, dst_rel_top); + int dst_bottom = term_row_rel_to_abs(term, dst_rel_bottom); + + /* Target area outside the screen is clipped */ + const size_t row_count = min(src_bottom - src_top, + dst_bottom - dst_top) + 1; + const size_t cell_count = min(src_right - src_left, + dst_right - dst_left) + 1; + + sixel_overwrite_by_rectangle( + term, dst_top, dst_left, row_count, cell_count); + + /* + * Copy source area + * + * Note: since source and destination may overlap, we need + * to copy out the entire source region first, and _then_ + * write the destination. I.e. this is similar to how + * memmove() behaves, but adapted to our row/cell + * structure. + */ + struct cell **copy = xmalloc(row_count * sizeof(copy[0])); + for (int r = 0; r < row_count; r++) { + copy[r] = xmalloc(cell_count * sizeof(copy[r][0])); + + const struct row *row = grid_row(term->grid, src_top + r); + const struct cell *cell = &row->cells[src_left]; + memcpy(copy[r], cell, cell_count * sizeof(copy[r][0])); + } + + /* Paste into destination area */ + for (int r = 0; r < row_count; r++) { + struct row *row = grid_row(term->grid, dst_top + r); + row->dirty = true; + + struct cell *cell = &row->cells[dst_left]; + memcpy(cell, copy[r], cell_count * sizeof(copy[r][0])); + free(copy[r]); + + for (;cell < &row->cells[dst_left + cell_count]; cell++) + cell->attrs.clean = 0; + + if (unlikely(row->extra != NULL)) { + /* TODO: technically, we should copy the source URIs... */ + grid_row_uri_range_erase(row, dst_left, dst_right); + } + } + free(copy); + break; + } + + case 'x': { /* DECFRA */ + const uint8_t c = vt_param_get(term, 0, 0); + + if (unlikely(!((c >= 32 && c < 126) || c >= 160))) + break; + + int top, left, bottom, right; + if (!params_to_rectangular_area( + term, 1, &top, &left, &bottom, &right)) + { + break; + } + + /* Erase the entire region at once (MUCH cheaper than + * doing it row by row, or even character by + * character). */ + sixel_overwrite_by_rectangle( + term, top, left, bottom - top + 1, right - left + 1); + + for (int r = top; r <= bottom; r++) + term_fill(term, r, left, c, right - left + 1, true); + + break; + } + + case 'z': { /* DECERA */ + int top, left, bottom, right; + if (!params_to_rectangular_area( + term, 0, &top, &left, &bottom, &right)) + { + break; + } + + /* + * Note: term_erase() _also_ erases sixels, but since + * we’re forced to erase one row at a time, erasing the + * entire sixel here is more efficient. + */ + sixel_overwrite_by_rectangle( + term, top, left, bottom - top + 1, right - left + 1); + + for (int r = top; r <= bottom; r++) + term_erase(term, r, left, r, right); + break; + } + } + + break; /* private[0] == ‘$’ */ + } + + case '#': { + switch (final) { + case 'P': { /* XTPUSHCOLORS */ + int slot = vt_param_get(term, 0, 0); + + /* Pm == 0, "push" (what xterm does is take take the + *current* slot + 1, even if that's in the middle of the + stack, and overwrites whatever is already in that + slot) */ + if (slot == 0) + slot = term->color_stack.idx + 1; + + if (term->color_stack.size < slot) { + const size_t new_size = slot; + term->color_stack.stack = xrealloc( + term->color_stack.stack, + new_size * sizeof(term->color_stack.stack[0])); + + /* Initialize new slots (except the selected slot, + which is done below) */ + xassert(new_size > 0); + for (size_t i = term->color_stack.size; i < new_size - 1; i++) { + memcpy(&term->color_stack.stack[i], &term->colors, + sizeof(term->colors)); + } + term->color_stack.size = new_size; + } + + xassert(slot > 0); + xassert(slot <= term->color_stack.size); + term->color_stack.idx = slot; + memcpy(&term->color_stack.stack[slot - 1], &term->colors, + sizeof(term->colors)); + break; + } + + case 'Q': { /* XTPOPCOLORS */ + int slot = vt_param_get(term, 0, 0); + + /* Pm == 0, "pop" (what xterm does is copy colors from the + *current* slot, *and* decrease the current slot index, + even if that's in the middle of the stack) */ + if (slot == 0) + slot = term->color_stack.idx; + + if (slot > 0 && slot <= term->color_stack.size) { + memcpy(&term->colors, &term->color_stack.stack[slot - 1], + sizeof(term->colors)); + term->color_stack.idx = slot - 1; + + /* Assume a full palette switch *will* affect almost + all cells. The alternative is to call + term_damage_color() for all 256 palette entries + *and* the default fg/bg (256 + 2 calls in total) */ + term_damage_view(term); + term_damage_margins(term); + } else if (slot == 0) { + LOG_ERR("XTPOPCOLORS: cannot pop beyond the first element"); + } else { + LOG_ERR( + "XTPOPCOLORS: invalid color slot: %d " + "(stack has %zu slots, current slot is %zu)", + vt_param_get(term, 0, 0), + term->color_stack.size, term->color_stack.idx); + } + break; + } + + case 'R': { /* XTREPORTCOLORS */ + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[?%zu;%zu#Q", + term->color_stack.idx, term->color_stack.size); + term_to_slave(term, reply, n); + break; + } + } + break; /* private[0] == '#' */ + } + + case 0x243f: /* ?$ */ + switch (final) { + case 'p': { + unsigned param = vt_param_get(term, 0, 0); + + /* + * Request DEC private mode (DECRQM) + * Reply: + * 0 - not recognized + * 1 - set + * 2 - reset + * 3 - permanently set + * 4 - permantently reset + */ + unsigned status = decrqm(term, param); + char reply[32]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[?%u;%u$y", param, status); + term_to_slave(term, reply, n); + break; + + } + + default: + UNHANDLED(); + break; + } + + break; /* private[0] == '?' && private[1] == '$' */ + + default: + UNHANDLED(); + break; + } +} diff --git a/csi.h b/csi.h new file mode 100644 index 0000000..12ae67e --- /dev/null +++ b/csi.h @@ -0,0 +1,6 @@ +#pragma once + +#include +#include "terminal.h" + +void csi_dispatch(struct terminal *term, uint8_t final); diff --git a/cursor-shape.c b/cursor-shape.c new file mode 100644 index 0000000..e68411c --- /dev/null +++ b/cursor-shape.c @@ -0,0 +1,130 @@ +#include +#include + +#define LOG_MODULE "cursor-shape" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "cursor-shape.h" +#include "debug.h" +#include "util.h" + +const char *const * +cursor_shape_to_string(enum cursor_shape shape) +{ + static const char *const table[][CURSOR_SHAPE_COUNT]= { + [CURSOR_SHAPE_NONE] = {NULL}, + [CURSOR_SHAPE_HIDDEN] = {"hidden", NULL}, + [CURSOR_SHAPE_LEFT_PTR] = {"default", "left_ptr", NULL}, + [CURSOR_SHAPE_POINTER] = {"pointer", "hand1", NULL}, + [CURSOR_SHAPE_TEXT] = {"text", "xterm", NULL}, + [CURSOR_SHAPE_TOP_LEFT_CORNER] = {"nw-resize", "top_left_corner", NULL}, + [CURSOR_SHAPE_TOP_RIGHT_CORNER] = {"ne-resize", "top_right_corner", NULL}, + [CURSOR_SHAPE_BOTTOM_LEFT_CORNER] = {"sw-resize", "bottom_left_corner", NULL}, + [CURSOR_SHAPE_BOTTOM_RIGHT_CORNER] = {"se-resize", "bottom_right_corner", NULL}, + [CURSOR_SHAPE_LEFT_SIDE] = {"w-resize", "left_side", NULL}, + [CURSOR_SHAPE_RIGHT_SIDE] = {"e-resize", "right_side", NULL}, + [CURSOR_SHAPE_TOP_SIDE] = {"n-resize", "top_side", NULL}, + [CURSOR_SHAPE_BOTTOM_SIDE] = {"s-resize", "bottom_side", NULL}, + + }; + + xassert(shape <= ALEN(table)); + return table[shape]; +} + +enum wp_cursor_shape_device_v1_shape +cursor_shape_to_server_shape(enum cursor_shape shape) +{ + static const enum wp_cursor_shape_device_v1_shape table[CURSOR_SHAPE_COUNT] = { + [CURSOR_SHAPE_LEFT_PTR] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT, + [CURSOR_SHAPE_POINTER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER, + [CURSOR_SHAPE_TEXT] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT, + [CURSOR_SHAPE_TOP_LEFT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NW_RESIZE, + [CURSOR_SHAPE_TOP_RIGHT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NE_RESIZE, + [CURSOR_SHAPE_BOTTOM_LEFT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SW_RESIZE, + [CURSOR_SHAPE_BOTTOM_RIGHT_CORNER] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SE_RESIZE, + [CURSOR_SHAPE_LEFT_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_W_RESIZE, + [CURSOR_SHAPE_RIGHT_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_E_RESIZE, + [CURSOR_SHAPE_TOP_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_N_RESIZE, + [CURSOR_SHAPE_BOTTOM_SIDE] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_S_RESIZE, + }; + + xassert(shape <= ALEN(table)); + xassert(table[shape] != 0); + return table[shape]; +} + +enum wp_cursor_shape_device_v1_shape +cursor_string_to_server_shape(const char *xcursor, int bound_version) +{ + if (xcursor == NULL) + return 0; + + static const char *const table[][2] = { + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT] = {"default", "left_ptr"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CONTEXT_MENU] = {"context-menu"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_HELP] = {"help", "question_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_POINTER] = {"pointer", "hand"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_PROGRESS] = {"progress", "left_ptr_watch"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_WAIT] = {"wait", "watch"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CELL] = {"cell"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_CROSSHAIR] = {"crosshair", "cross"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT] = {"text", "xterm"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_VERTICAL_TEXT] = {"vertical-text"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALIAS] = {"alias", "dnd-link"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_COPY] = {"copy", "dnd-copy"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_MOVE] = {"move", "dnd-move"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NO_DROP] = {"no-drop", "dnd-no-drop"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NOT_ALLOWED] = {"not-allowed", "crossed_circle"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRAB] = {"grab", "hand1"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_GRABBING] = {"grabbing"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_E_RESIZE] = {"e-resize", "right_side"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_N_RESIZE] = {"n-resize", "top_side"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NE_RESIZE] = {"ne-resize", "top_right_corner"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NW_RESIZE] = {"nw-resize", "top_left_corner"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_S_RESIZE] = {"s-resize", "bottom_side"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SE_RESIZE] = {"se-resize", "bottom_right_corner"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_SW_RESIZE] = {"sw-resize", "bottom_left_corner"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_W_RESIZE] = {"w-resize", "left_side"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_EW_RESIZE] = {"ew-resize", "sb_h_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NS_RESIZE] = {"ns-resize", "sb_v_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NESW_RESIZE] = {"nesw-resize", "fd_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_NWSE_RESIZE] = {"nwse-resize", "bd_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_COL_RESIZE] = {"col-resize", "sb_h_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ROW_RESIZE] = {"row-resize", "sb_v_double_arrow"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_SCROLL] = {"all-scroll", "fleur"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ZOOM_IN] = {"zoom-in"}, + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ZOOM_OUT] = {"zoom-out"}, +#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION) /* 1.42 */ + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK] = {"dnd-ask"}, +#endif +#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE_SINCE_VERSION) /* 1.42 */ + [WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE] = {"all-resize"}, +#endif + }; + + for (size_t i = 0; i < ALEN(table); i++) { +#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION) + if (i == WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK && + bound_version < WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION) + { + continue; + } +#endif +#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE_SINCE_VERSION) + if (i == WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE && + bound_version < WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_ALL_RESIZE_SINCE_VERSION) + { + continue; + } +#endif + for (size_t j = 0; j < ALEN(table[i]); j++) { + if (table[i][j] != NULL && streq(xcursor, table[i][j])) { + return i; + } + } + } + + return 0; +} diff --git a/cursor-shape.h b/cursor-shape.h new file mode 100644 index 0000000..51a411e --- /dev/null +++ b/cursor-shape.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +enum cursor_shape { + CURSOR_SHAPE_NONE, + CURSOR_SHAPE_CUSTOM, + CURSOR_SHAPE_HIDDEN, + + CURSOR_SHAPE_LEFT_PTR, + CURSOR_SHAPE_POINTER, + CURSOR_SHAPE_TEXT, + CURSOR_SHAPE_TOP_LEFT_CORNER, + CURSOR_SHAPE_TOP_RIGHT_CORNER, + CURSOR_SHAPE_BOTTOM_LEFT_CORNER, + CURSOR_SHAPE_BOTTOM_RIGHT_CORNER, + CURSOR_SHAPE_LEFT_SIDE, + CURSOR_SHAPE_RIGHT_SIDE, + CURSOR_SHAPE_TOP_SIDE, + CURSOR_SHAPE_BOTTOM_SIDE, + + CURSOR_SHAPE_COUNT, +}; + +const char *const *cursor_shape_to_string(enum cursor_shape shape); + +enum wp_cursor_shape_device_v1_shape cursor_shape_to_server_shape( + enum cursor_shape shape); +enum wp_cursor_shape_device_v1_shape cursor_string_to_server_shape( + const char *xcursor, int bound_version); diff --git a/dcs.c b/dcs.c new file mode 100644 index 0000000..376c73b --- /dev/null +++ b/dcs.c @@ -0,0 +1,533 @@ +#include "dcs.h" +#include + +#define LOG_MODULE "dcs" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "foot-terminfo.h" +#include "sixel.h" +#include "util.h" +#include "vt.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +static bool +ensure_size(struct terminal *term, size_t required_size) +{ + if (required_size <= term->vt.dcs.size) + return true; + + uint8_t *new_data = realloc(term->vt.dcs.data, required_size); + if (new_data == NULL) { + LOG_ERRNO("failed to increase size of DCS buffer"); + return false; + } + + term->vt.dcs.data = new_data; + term->vt.dcs.size = required_size; + return true; +} + +/* Decode hex-encoded string *inline*. NULL terminates */ +static char * +hex_decode(const char *s, size_t len) +{ + if (len % 2) + return NULL; + + char *hex = xmalloc(len / 2 + 1); + char *o = hex; + + /* TODO: error checking */ + for (size_t i = 0; i < len; i += 2) { + uint8_t nib1 = hex2nibble(*s); s++; + uint8_t nib2 = hex2nibble(*s); s++; + + if (nib1 == HEX_DIGIT_INVALID || nib2 == HEX_DIGIT_INVALID) + goto err; + + *o = nib1 << 4 | nib2; o++; + } + + *o = '\0'; + return hex; + +err: + free(hex); + return NULL; +} + +UNITTEST +{ + /* Verify table is sorted */ + const char *p = terminfo_capabilities; + size_t left = sizeof(terminfo_capabilities); + + const char *last_cap = NULL; + + while (left > 0) { + const char *cap = p; + const char *val = cap + strlen(cap) + 1; + + size_t size = strlen(cap) + 1 + strlen(val) + 1;; + xassert(size <= left); + p += size; + left -= size; + + if (last_cap != NULL) + xassert(strcmp(last_cap, cap) < 0); + + last_cap = cap; + } +} + +static bool +lookup_capability(const char *name, const char **value) +{ + const char *p = terminfo_capabilities; + size_t left = sizeof(terminfo_capabilities); + + while (left > 0) { + const char *cap = p; + const char *val = cap + strlen(cap) + 1; + + size_t size = strlen(cap) + 1 + strlen(val) + 1;; + xassert(size <= left); + p += size; + left -= size; + + int r = strcmp(cap, name); + if (r == 0) { + *value = val; + return true; + } else if (r > 0) + break; + } + + *value = NULL; + return false; +} + +static void +xtgettcap_reply(struct terminal *term, const char *hex_cap_name, size_t len) +{ + char *name = hex_decode(hex_cap_name, len); + if (name == NULL) { + LOG_WARN("XTGETTCAP: invalid hex encoding, ignoring capability"); + return; + } + + const char *value; + bool valid_capability = lookup_capability(name, &value); + xassert(!valid_capability || value != NULL); + + LOG_DBG("XTGETTCAP: cap=%s (%.*s), value=%s", + name, (int)len, hex_cap_name, + valid_capability ? value : ""); + + if (!valid_capability) + goto err; + + if (value[0] == '\0') { + /* Boolean */ + term_to_slave(term, "\033P1+r", 5); + term_to_slave(term, hex_cap_name, len); + term_to_slave(term, "\033\\", 2); + goto out; + } + + /* + * Reply format: + * \EP 1 + r cap=value \E\\ + * Where 'cap' and 'value are hex encoded ascii strings + */ + char *reply = xmalloc( + 5 + /* DCS 1 + r (\EP1+r) */ + len + /* capability name, hex encoded */ + 1 + /* '=' */ + strlen(value) * 2 + /* capability value, hex encoded */ + 2 + /* ST (\E\\) */ + 1); + + int idx = sprintf(reply, "\033P1+r%.*s=", (int)len, hex_cap_name); + + for (const char *c = value; *c != '\0'; c++) { + uint8_t nib1 = (uint8_t)*c >> 4; + uint8_t nib2 = (uint8_t)*c & 0xf; + + reply[idx] = nib1 >= 0xa ? 'A' + nib1 - 0xa : '0' + nib1; idx++; + reply[idx] = nib2 >= 0xa ? 'A' + nib2 - 0xa : '0' + nib2; idx++; + } + + reply[idx] = '\033'; idx++; + reply[idx] = '\\'; idx++; + term_to_slave(term, reply, idx); + + free(reply); + goto out; + +err: + term_to_slave(term, "\033P0+r", 5); + term_to_slave(term, hex_cap_name, len); + term_to_slave(term, "\033\\", 2); + +out: + free(name); +} + +static void +xtgettcap_put(struct terminal *term, uint8_t c) +{ + struct vt *vt = &term->vt; + + /* Grow buffer expontentially */ + if (vt->dcs.idx >= vt->dcs.size) { + size_t new_size = vt->dcs.size * 2; + if (new_size == 0) + new_size = 128; + + if (!ensure_size(term, new_size)) + return; + } + + vt->dcs.data[vt->dcs.idx++] = c; +} + +static void +xtgettcap_unhook(struct terminal *term) +{ + size_t left = term->vt.dcs.idx; + + const char *const end = (const char *)&term->vt.dcs.data[left]; + const char *p = (const char *)term->vt.dcs.data; + + if (p == NULL) { + /* Request is empty; send an error reply, without any capabilities */ + term_to_slave(term, "\033P0+r\033\\", 7); + return; + } + + while (true) { + const char *sep = memchr(p, ';', left); + size_t cap_len; + + if (sep == NULL) { + /* Last capability */ + cap_len = end - p; + } else { + cap_len = sep - p; + } + + xtgettcap_reply(term, p, cap_len); + + left -= cap_len + 1; + p += cap_len + 1; + + if (sep == NULL) + break; + } +} + +static void NOINLINE +append_sgr_attr_n(char **reply, size_t *len, const char *attr, size_t n) +{ + size_t new_len = *len + n + 1; + *reply = xrealloc(*reply, new_len); + memcpy(&(*reply)[*len], attr, n); + (*reply)[new_len - 1] = ';'; + *len = new_len; +} + +static void +decrqss_put(struct terminal *term, uint8_t c) +{ + /* Largest request we support is two bytes */ + if (!ensure_size(term, 2)) + return; + + struct vt *vt = &term->vt; + if (vt->dcs.idx >= 2) + return; + vt->dcs.data[vt->dcs.idx++] = c; +} + +static void +decrqss_unhook(struct terminal *term) +{ + const uint8_t *query = term->vt.dcs.data; + const size_t n = term->vt.dcs.idx; + + /* + * A note on the Ps parameter in the reply: many DEC manual + * instances (e.g. https://vt100.net/docs/vt510-rm/DECRPSS) claim + * that 0 means "request is valid", and 1 means "request is + * invalid". + * + * However, this appears to be a typo; actual hardware inverts the + * response (as does XTerm and mlterm): + * https://github.com/hackerb9/vt340test/issues/13 + */ + + if (n == 1 && query[0] == 'r') { + /* DECSTBM - Set Top and Bottom Margins */ + char reply[64]; + size_t len = xsnprintf(reply, sizeof(reply), "\033P1$r%d;%dr\033\\", + term->scroll_region.start + 1, + term->scroll_region.end); + term_to_slave(term, reply, len); + } + + else if (n == 1 && query[0] == 'm') { + /* SGR - Set Graphic Rendition */ + char *reply = NULL; + size_t len = 0; + + #define append_sgr_attr(num_as_str) \ + append_sgr_attr_n(&reply, &len, num_as_str, sizeof(num_as_str) - 1) + + /* Always present, both in the example from the VT510 manual + * (https://vt100.net/docs/vt510-rm/DECRPSS), and in XTerm and + * mlterm */ + append_sgr_attr("0"); + + struct attributes *a = &term->vt.attrs; + if (a->bold) + append_sgr_attr("1"); + if (a->dim) + append_sgr_attr("2"); + if (a->italic) + append_sgr_attr("3"); + if (a->underline) { + if (term->vt.underline.style > UNDERLINE_SINGLE) { + char value[4]; + size_t val_len = + xsnprintf(value, sizeof(value), "4:%d", term->vt.underline.style); + append_sgr_attr_n(&reply, &len, value, val_len); + } else + append_sgr_attr("4"); + } + if (a->blink) + append_sgr_attr("5"); + if (a->reverse) + append_sgr_attr("7"); + if (a->conceal) + append_sgr_attr("8"); + if (a->strikethrough) + append_sgr_attr("9"); + + switch (a->fg_src) { + case COLOR_DEFAULT: + break; + + case COLOR_BASE16: { + char value[4]; + size_t val_len = xsnprintf( + value, sizeof(value), "%u", + a->fg >= 8 ? a->fg - 8 + 90 : a->fg + 30); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + + case COLOR_BASE256: { + char value[16]; + size_t val_len = xsnprintf(value, sizeof(value), "38:5:%u", a->fg); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + + case COLOR_RGB: { + uint8_t r = a->fg >> 16; + uint8_t g = a->fg >> 8; + uint8_t b = a->fg >> 0; + + char value[32]; + size_t val_len = xsnprintf( + value, sizeof(value), "38:2::%hhu:%hhu:%hhu", r, g, b); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + } + + switch (a->bg_src) { + case COLOR_DEFAULT: + break; + + case COLOR_BASE16: { + char value[4]; + size_t val_len = xsnprintf( + value, sizeof(value), "%u", + a->bg >= 8 ? a->bg - 8 + 100 : a->bg + 40); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + + case COLOR_BASE256: { + char value[16]; + size_t val_len = xsnprintf(value, sizeof(value), "48:5:%u", a->bg); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + + case COLOR_RGB: { + uint8_t r = a->bg >> 16; + uint8_t g = a->bg >> 8; + uint8_t b = a->bg >> 0; + + char value[32]; + size_t val_len = xsnprintf( + value, sizeof(value), "48:2::%hhu:%hhu:%hhu", r, g, b); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + } + + switch (term->vt.underline.color_src) { + case COLOR_DEFAULT: + case COLOR_BASE16: + break; + + case COLOR_BASE256: { + char value[16]; + size_t val_len = xsnprintf( + value, sizeof(value), "58:5:%u", term->vt.underline.color); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + + case COLOR_RGB: { + uint8_t r = term->vt.underline.color >> 16; + uint8_t g = term->vt.underline.color >> 8; + uint8_t b = term->vt.underline.color >> 0; + + char value[32]; + size_t val_len = xsnprintf( + value, sizeof(value), "58:2::%hhu:%hhu:%hhu", r, g, b); + append_sgr_attr_n(&reply, &len, value, val_len); + break; + } + } + + #undef append_sgr_attr_n + + reply[len - 1] = 'm'; + + term_to_slave(term, "\033P1$r", 5); + term_to_slave(term, reply, len); + term_to_slave(term, "\033\\", 2); + free(reply); + } + + else if (n == 2 && memcmp(query, " q", 2) == 0) { + /* DECSCUSR - Set Cursor Style */ + int mode; + + switch (term->cursor_style) { + case CURSOR_HOLLOW: /* FALLTHROUGH */ + case CURSOR_BLOCK: mode = 2; break; + case CURSOR_UNDERLINE: mode = 4; break; + case CURSOR_BEAM: mode = 6; break; + default: BUG("invalid cursor style"); break; + } + + if (term->cursor_blink.deccsusr) + mode--; + + char reply[16]; + size_t len = xsnprintf(reply, sizeof(reply), "\033P1$r%d q\033\\", mode); + term_to_slave(term, reply, len); + } + + else { + static const char err[] = "\033P0$r\033\\"; + term_to_slave(term, err, sizeof(err) - 1); + } +} + +void +dcs_hook(struct terminal *term, uint8_t final) +{ + LOG_DBG("hook: %c (intermediate(s): %.2s, param=%d)", final, + (const char *)&term->vt.private, vt_param_get(term, 0, 0)); + + xassert(term->vt.dcs.data == NULL); + xassert(term->vt.dcs.size == 0); + xassert(term->vt.dcs.put_handler == NULL); + xassert(term->vt.dcs.unhook_handler == NULL); + + switch (term->vt.private) { + case 0: + switch (final) { + case 'q': { + if (!term->conf->tweak.sixel) { + break; + } + int p1 = vt_param_get(term, 0, 0); + int p2 = vt_param_get(term, 1, 0); + int p3 = vt_param_get(term, 2, 0); + + term->vt.dcs.put_handler = sixel_init(term, p1, p2, p3); + term->vt.dcs.unhook_handler = &sixel_unhook; + break; + } + } + break; + + case '$': + switch (final) { + case 'q': + term->vt.dcs.put_handler = &decrqss_put; + term->vt.dcs.unhook_handler = &decrqss_unhook; + break; + } + break; + + case '=': + switch (final) { + case 's': + /* BSU/ESU: https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec */ + switch (vt_param_get(term, 0, 0)) { + case 1: + term->vt.dcs.unhook_handler = &term_enable_app_sync_updates; + return; + case 2: + term->vt.dcs.unhook_handler = &term_disable_app_sync_updates; + return; + } + break; + } + break; + + case '+': + switch (final) { + case 'q': /* XTGETTCAP */ + term->vt.dcs.put_handler = &xtgettcap_put; + term->vt.dcs.unhook_handler = &xtgettcap_unhook; + break; + } + break; + } +} + +void +dcs_put(struct terminal *term, uint8_t c) +{ + /* LOG_DBG("PUT: %c", c); */ + + if (term->vt.dcs.put_handler != NULL) + term->vt.dcs.put_handler(term, c); +} + +void +dcs_unhook(struct terminal *term) +{ + if (term->vt.dcs.unhook_handler != NULL) + term->vt.dcs.unhook_handler(term); + + term->vt.dcs.unhook_handler = NULL; + term->vt.dcs.put_handler = NULL; + + free(term->vt.dcs.data); + term->vt.dcs.data = NULL; + term->vt.dcs.size = 0; + term->vt.dcs.idx = 0; +} diff --git a/dcs.h b/dcs.h new file mode 100644 index 0000000..f89de38 --- /dev/null +++ b/dcs.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include "terminal.h" + +void dcs_hook(struct terminal *term, uint8_t final); +void dcs_put(struct terminal *term, uint8_t c); +void dcs_unhook(struct terminal *term); diff --git a/debug.c b/debug.c new file mode 100644 index 0000000..8a86e9d --- /dev/null +++ b/debug.c @@ -0,0 +1,47 @@ +#include "debug.h" + +#include +#include +#include +#include +#include +#include "log.h" + +#if defined(__SANITIZE_ADDRESS__) || HAS_FEATURE(address_sanitizer) +#include +#define ASAN_ENABLED 1 +#endif + +static void +print_stack_trace(void) +{ +#ifdef ASAN_ENABLED + fputs("\nStack trace:\n", stderr); + __sanitizer_print_stack_trace(); +#endif +} + +noreturn void +fatal_error(const char *file, int line, const char *msg, int err) +{ + log_msg(LOG_CLASS_ERROR, "debug", file, line, "%s: %s", msg, strerror(err)); + print_stack_trace(); + fflush(stderr); + abort(); +} + +noreturn void +bug(const char *file, int line, const char *func, const char *fmt, ...) +{ + char buf[4096]; + va_list ap; + va_start(ap, fmt); + int n = vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + + const char *msg = likely(n >= 0) ? buf : "??"; + log_msg(LOG_CLASS_ERROR, "debug", file, line, "BUG in %s(): %s", func, msg); + print_stack_trace(); + fflush(stderr); + abort(); +} diff --git a/debug.h b/debug.h new file mode 100644 index 0000000..92a0e09 --- /dev/null +++ b/debug.h @@ -0,0 +1,32 @@ +#pragma once + +#include "macros.h" + +#define FATAL_ERROR(...) fatal_error(__FILE__, __LINE__, __VA_ARGS__) + +#ifdef NDEBUG + #define BUG(...) UNREACHABLE() +#else + #define BUG(...) bug(__FILE__, __LINE__, __func__, __VA_ARGS__) +#endif + +#define xassert(x) do { \ + IGNORE_WARNING("-Wtautological-compare") \ + if (unlikely(!(x))) { \ + BUG("assertion failed: '%s'", #x); \ + } \ + UNIGNORE_WARNINGS \ +} while (0) + +#ifndef static_assert + #if __STDC_VERSION__ >= 201112L + #define static_assert(x, msg) _Static_assert((x), msg) + #elif GNUC_AT_LEAST(4, 6) || HAS_EXTENSION(c_static_assert) + #define static_assert(x, msg) __extension__ _Static_assert((x), msg) + #else + #define static_assert(x, msg) + #endif +#endif + +noreturn void fatal_error(const char *file, int line, const char *msg, int err) COLD; +noreturn void bug(const char *file, int line, const char *func, const char *fmt, ...) PRINTF(4) COLD; diff --git a/doc/benchmark.md b/doc/benchmark.md new file mode 100644 index 0000000..f77b826 --- /dev/null +++ b/doc/benchmark.md @@ -0,0 +1,81 @@ +# Benchmarks + +## vtebench + +All benchmarks are done using [vtebench](https://github.com/alacritty/vtebench): + +```sh +./target/release/vtebench -b ./benchmarks --dat /tmp/ +``` + +## 2022-05-12 + +### System + +CPU: i9-9900 + +RAM: 64GB + +Graphics: Radeon RX 5500XT + + +### Terminal configuration + +Geometry: 2040x1884 + +Font: Fantasque Sans Mono 10.00pt/23px + +Scrollback: 10000 lines + + +### Results + +| Benchmark (times in ms) | Foot (GCC+PGO) 1.12.1 | Foot 1.12.1 | Alacritty 0.10.1 | URxvt 9.26 | XTerm 372 | +|-------------------------------|----------------------:|------------:|-----------------:|-----------:|----------:| +| cursor motion | 10.40 | 14.07 | 24.97 | 23.38 | 1622.86 | +| dense cells | 29.58 | 45.46 | 97.45 | 10828.00 | 2323.00 | +| light cells | 4.34 | 4.40 | 12.84 | 12.17 | 49.81 | +| scrollling | 135.31 | 116.35 | 121.69 | 108.30 | 4041.33 | +| scrolling bottom region | 118.19 | 109.70 | 105.26 | 118.80 | 3875.00 | +| scrolling bottom small region | 132.41 | 122.11 | 122.83 | 151.30 | 3839.67 | +| scrolling fullscreen | 5.70 | 5.66 | 10.92 | 12.09 | 124.25 | +| scrolling top region | 144.19 | 121.78 | 135.81 | 159.24 | 3858.33 | +| scrolling top small region | 135.95 | 119.01 | 115.46 | 216.55 | 3872.67 | +| unicode | 11.56 | 10.92 | 15.94 | 1012.27 | 4779.33 | + + +## 2022-05-12 + +### System + +CPU: i5-8250U + +RAM: 8GB + +Graphics: Intel UHD Graphics 620 + + +### Terminal configuration + +Geometry: 945x1020 + +Font: Dina:pixelsize=12 + +Scrollback=10000 lines + + +### Results + + +| Benchmark (times in ms) | Foot (GCC+PGO) 1.12.1 | Foot 1.12.1 | Alacritty 0.10.1 | URxvt 9.26 | XTerm 372 | +|-------------------------------|----------------------:|------------:|-----------------:|-----------:|----------:| +| cursor motion | 15.03 | 16.74 | 23.22 | 24.14 | 1381.63 | +| dense cells | 43.56 | 54.10 | 89.43 | 1807.17 | 1945.50 | +| light cells | 7.96 | 9.66 | 20.19 | 21.31 | 122.44 | +| scrollling | 146.02 | 150.47 | 129.22 | 129.84 | 10140.00 | +| scrolling bottom region | 138.36 | 137.42 | 117.06 | 141.87 | 10136.00 | +| scrolling bottom small region | 137.40 | 134.66 | 128.97 | 208.77 | 9930.00 | +| scrolling fullscreen | 11.66 | 12.02 | 19.69 | 21.96 | 315.80 | +| scrolling top region | 143.81 | 133.47 | 132.51 | 475.81 | 10267.00 | +| scrolling top small region | 133.72 | 135.32 | 145.10 | 314.13 | 10074.00 | +| unicode | 20.89 | 21.78 | 26.11 | 5687.00 | 15740.00 | diff --git a/doc/foot-ctlseqs.7.scd b/doc/foot-ctlseqs.7.scd new file mode 100644 index 0000000..40906eb --- /dev/null +++ b/doc/foot-ctlseqs.7.scd @@ -0,0 +1,813 @@ +foot-ctlseqs(7) + +# NAME +foot-ctlseqs - terminal control sequences supported by foot + +# DESCRIPTION + +This document describes all the control sequences supported by foot. + +- Control characters +- Sequences beginning with ESC +- CSI - Control Sequence Introducer + - SGR + - Indexed and RGB colors (256-color palette and 24-bit colors) + - Private modes + - Window manipulation + - Other +- OSC - Operating System Command +- DCS - Device Control String + +# Control characters + +[[ *Sequence* +:[ *Name* +:< *Description* +| \\a +: BEL +: Depends on what *bell* in *foot.ini*(5) is set to. +| \\b +: BS +: Backspace; move the cursor left one step. Wrap if _bw_ is enabled. +| \\t +: HT +: Horizontal tab; move the cursor to the next tab stop. +| \\n +: LF +: Line feed; move the cursor down one step, or scroll content up if + at the bottom line. +| \\v +: VT +: Vertical tab; identical to _LF_. +| \\f +: FF +: Form feed; identical to _LF_. +| \\r +: CR +: Carriage ret; move the cursor to the leftmost column. +| \\x0E +: SO +: Shift out; select the _G1_ character set. +| \\x0F +: SI +: Shift in; select the _G0_ character set. + +# Sequences beginning with ESC + +Note: this table excludes sequences where ESC is part of a 7-bit +equivalent to 8-bit C1 controls. + +[[ *Sequence* +:[ *Name* +:[ *Origin* +:< *Description* +| \\E 7 +: DECSC +: VT100 +: Save cursor position. +| \\E 8 +: DECRC +: VT100 +: Restore cursor position. +| \\E c +: RIS +: VT100 +: Reset terminal to initial state. +| \\E D +: IND +: VT100 +: Line feed; move the cursor down one step, or scroll content up if + at the bottom margin. +| \\E E +: NEL +: VT100 +: Next line; move the cursor down one step, and to the first + column. Content is scrolled up if at the bottom line. +| \\E H +: HTS +: VT100 +: Set one horizontal tab stop at the current position. +| \\E M +: RI +: VT100 +: Reverse index; move the cursor up one step, or scroll content down + if at the top margin. +| \\E N +: SS2 +: VT220 +: Single shift select of G2 character set (affects next character only). +| \\E O +: SS3 +: VT220 +: Single shift select of G3 character set (affects next character only). +| \\E = +: DECKPAM +: VT100 +: Switch keypad to _application_ mode. +| \\E > +: DECKPNM +: VT100 +: Switch keypad to _numeric_ mode. +| \\E ( _C_ +: SCS +: VT100 +: Designate G0 character set. Supported values for _C_ are: *0* (DEC + Special Character and Line Drawing Set), and *B* (USASCII). +| \\E ) _C_ +: SCS +: VT100 +: Designate G1 character set. Same supported values for _C_ as in _G0_. +| \\E \* _C_ +: SCS +: VT220 +: Designate G2 character set. Same supported values for _C_ as in _G0_. +| \\E + _C_ +: SCS +: VT220 +: Designate G3 character set. Same supported values for _C_ as in _G0_. + +# CSI + +All sequences begin with *\\E[*, sometimes abbreviated "CSI". Spaces +are used in the sequence strings to make them easier to read, but are +not actually part of the string (i.e. *\\E[ 1 m* is really *\\E[1m*). + +## SGR + +All SGR sequences are in the form *\\E[* _N_ *m*, where _N_ is a decimal +number - the _parameter_. Multiple parameters can be combined in a +single CSI sequence by separating them with semicolons: *\\E[ 1;2;3 +m*. + +[[ *Parameter* +:< *Description* +| 0 +: Reset all attributes +| 1 +: Bold +| 2 +: Dim +| 3 +: Italic +| 4 +: Underline, including styled underlines +| 5 +: Blink +| 7 +: Reverse video; swap foreground and background colors +| 8 +: Conceal; text is not visible, but is copiable +| 9 +: Crossed-out/strike +| 21 +: Double underline +| 22 +: Disable *bold* and *dim* +| 23 +: Disable italic +| 24 +: Disable underline +| 25 +: Disable blink +| 27 +: Disable reverse video +| 28 +: Disable conceal +| 29 +: Disable crossed-out +| 30-37 +: Select foreground color (using *regularN* in *foot.ini*(5)) +| 38 +: Select foreground color, see "indexed and RGB colors" below +| 39 +: Use the default foreground color (*foreground* in *foot.ini*(5)) +| 40-47 +: Select background color (using *regularN* in *foot.ini*(5)) +| 48 +: Select background color, see "indexed and RGB colors" below +| 49 +: Use the default background color (*background* in *foot.ini*(5)) +| 58 +: Select underline color, see "indexed and RGB colors" below +| 59 +: Use the default underline color +| 90-97 +: Select foreground color (using *brightN* in *foot.ini*(5)) +| 100-107 +: Select background color (using *brightN* in *foot.ini*(5)) + +## Indexed and RGB colors (256-color palette and 24-bit colors) + +Foot supports both the new sub-parameter based variants, and the older +parameter based variants for setting foreground and background colors. + +Indexed colors: + +- *\\E[ 38 : 5 :* _idx_ *m* +- *\\E[ 38 ; 5 ;* _idx_ *m* + +RGB colors: + +- *\\E[ 38 : 2 :* _cs_ *:* _r_ *:* _g_ *:* _b_ *m* +- *\\E[ 38 : 2 :* _r_ *:* _g_ *:* _b_ *m* +- *\\E[ 38 ; 2 ;* _r_ *;* _g_ *;* _b_ *m* + +The first variant is the "correct" one (and foot also recognizes, but +ignores, the optional _tolerance_ parameters). + +The second one is allowed since many programs "forget" the color space +ID, _cs_. + +The sub-parameter based variants are preferred, and are what foot's +*terminfo*(5) entry uses. + +## Private Modes + +There are several Boolean-like "modes" that affect certain aspects +of the terminal's behavior. These modes can be manipulated with the +following 4 escape sequences: + +[[ *Sequence* +:[ *Name* +:< *Description* +| \\E[ ? _Pm_ h +: DECSET +: Enable private mode +| \\E[ ? _Pm_ l +: DECRST +: Disable private mode +| \\E[ ? _Pm_ s +: XTSAVE +: Save private mode +| \\E[ ? _Pm_ r +: XTRESTORE +: Restore private mode + + +The _Pm_ parameter in the above sequences denotes a numerical ID +that corresponds to one of the following modes: + +[[ *Parameter* +:[ *Origin* +:< *Description* +| 1 +: VT100 +: Cursor keys mode (DECCKM) +| 5 +: VT100 +: Reverse video (DECSCNM) +| 6 +: VT100 +: Origin mode (DECOM) +| 7 +: VT100 +: Auto-wrap mode (DECAWM) +| 12 +: AT&T 610 +: Cursor blink +| 25 +: VT220 +: Cursor visibility (DECTCEM) +| 45 +: xterm +: Reverse-wraparound mode +| 47 +: xterm +: Same as 1047 (see below) +| 66 +: VT320 +: Numeric keypad mode (DECNKM); same as DECKPAM/DECKPNM when enabled/disabled +| 1000 +: xterm +: Send mouse x/y on button press/release +| 1001 +: xterm +: Use hilite mouse tracking +| 1002 +: xterm +: Use cell motion mouse tracking +| 1003 +: xterm +: Use all motion mouse tracking +| 1004 +: xterm +: Send FocusIn/FocusOut events +| 1006 +: xterm +: SGR mouse mode +| 1007 +: xterm +: Alternate scroll mode +| 1015 +: urxvt +: urxvt mouse mode +| 1016 +: xterm +: SGR-Pixels mouse mode +| 1034 +: xterm +: 8-bit Meta mode +| 1035 +: xterm +: Num Lock modifier (see xterm numLock option) +| 1036 +: xterm +: Send ESC when Meta modifies a key (see xterm metaSendsEscape option) +| 1042 +: xterm +: Perform action for BEL character (see *bell* in *foot.ini*(5)) +| 1047 +: xterm +: Use alternate screen buffer +| 1048 +: xterm +: Save/restore cursor (DECSET=save, DECRST=restore) +| 1049 +: xterm +: Equivalent to 1048 and 1047 combined +| 1070 +: xterm +: Use private color registers for each sixel +| 2004 +: xterm +: Wrap pasted text with start/end delimiters (bracketed paste mode) +| 2026 +: terminal-wg +: Application synchronized updates mode +| 2027 +: contour +: Grapheme cluster processing +| 2031 +: contour +: Request color theme updates +| 2048 +: TODO +: In-band window resize notifications +| 8452 +: xterm +: Position cursor to the right of sixels, instead of on the next line +| 737769 +: foot +: Input Method Editor (IME) mode + +## Window manipulation + +Foot implements a sub-set of XTerm's (originally dtterm's) window +manipulation sequences. The generic format is: + +*\\E[ *_Ps_* ; *_Ps_* ; *_Ps_* t* + +[[ *Parameter 1* +:[ *Parameter 2* +:< *Description* +| 11 +: - +: Report if window is iconified. Foot always reports *1* - not iconified. +| 13 +: - +: Report window position. Foot always reports (0,0), due to Wayland + limitations. +| 13 +: 2 +: Report text area position. Foot always reports (0,0) due to Wayland + limitations. +| 14 +: - +: Report text area size, in pixels. Foot reports the grid size, + excluding the margins. +| 14 +: 2 +: Report window size, in pixels. Foot reports the grid size plus the + margins. +| 15 +: - +: Report the screen size, in pixels. +| 16 +: - +: Report the cell size, in pixels. +| 18 +: - +: Report text area size, in characters. +| 19 +: - +: Report screen size, in characters. +| 20 +: - +: Report icon label. +| 22 +: - +: Push window title+icon. +| 22 +: 1 +: Push window icon. +| 22 +: 2 +: Push window title. +| 23 +: - +: Pop window title+icon. +| 23 +: 1 +: Pop window icon. +| 23 +: 2 +: Pop window title. + +## Other + +[[ *Parameter* +:[ *Name* +:[ *Origin* +:< *Description* +| \\E[ _Ps_ c +: DA +: VT100 +: Send primary device attributes. Foot responds with "I'm a VT220 with + sixel and ANSI color support". +| \\E[ _Ps_ A +: CUU +: VT100 +: Cursor up - move cursor up _Ps_ times. +| \\E[ _Ps_ B +: CUD +: VT100 +: Cursor down - move cursor down _Ps_ times. +| \\E[ _Ps_ C +: CUF +: VT100 +: Cursor forward - move cursor to the right _Ps_ times. +| \\E[ _Ps_ D +: CUB +: VT100 +: Cursor backward - move cursor to the left _Ps_ times. +| \\E[ _Ps_ g +: TBC +: VT100 +: Tab clear. _Ps_=0 -> clear current column. _Ps_=3 -> clear all. +| \\E[ _Ps_ ; _Ps_ f +: HVP +: VT100 +: Horizontal and vertical position - move cursor to _row_ ; _column_. +| \\E[ _Ps_ ; _Ps_ H +: CUP +: VT100 +: Cursor position - move cursor to _row_ ; _column_. +| \\E[ _Ps_ J +: ED +: VT100 +: Erase in display. _Ps_=0 -> below cursor. _Ps_=1 -> above +| \\E[ _Ps_ K +: EL +: VT100 +: Erase in line. _Ps_=0 -> right of cursor. _Ps_=1 -> left of + cursor. _Ps_=2 -> all. +| \\E[ _Pm_ h +: SM +: VT100 +: Set mode. _Pm_=4 -> enable IRM (Insertion Replacement Mode). All + other values of _Pm_ are unsupported. +| \\E[ _Pm_ l +: RM +: VT100 +: Reset mode. _Pm_=4 -> disable IRM (Insertion Replacement Mode). All + other values of _Pm_ are unsupported. +| \\E[ _Ps_ n +: DSR +: VT100 +: Device status report. _Ps_=5 -> device status. _Ps_=6 -> cursor + position. +| \\E[ _Ps_ L +: IL +: VT220 +: Insert _Ps_ lines. +| \\E[ _Ps_ M +: DL +: VT220 +: Delete _Ps_ lines. +| \\E[ _Ps_ P +: DCH +: VT220 +: Delete _Ps_ characters. +| \\E[ _Ps_ @ +: ICH +: VT220 +: Insert _Ps_ blank characters. +| \\E[ _Ps_ X +: ECH +: VT220 +: Erase _Ps_ characters. +| \\E[ > c +: DA2 +: VT220 +: Send secondary device attributes. Foot responds with "I'm a VT220 + and here's my version number". +| \\E[ ! p +: DECSTR +: VT220 +: Soft terminal reset. +| \\E[ ? _Ps_ $ p +: DECRQM +: VT320 +: Request status of DEC private mode. The _Ps_ parameter corresponds + to one of the values mentioned in the "Private Modes" section above + (as set with DECSET/DECRST). +| \\E[ _Ps_ $ p +: DECRQM +: VT320 +: Request status of ECMA-48/ANSI mode. See the descriptions for SM/RM + above for recognized _Ps_ values. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pm_ $ r +: DECCARA +: VT400 +: Change attributes in rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ + denotes the rectangle, _Pm_ denotes the SGR attributes. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pm_ $ t +: DECRARA +: VT400 +: Invert attributes in rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ + denotes the rectangle, _Pm_ denotes the SGR attributes. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pp_ ; _Pt_ ; _Pl_ ; _Pp_ $ v +: DECCRA +: VT400 +: Copy rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ denotes the + rectangle, _Pt_ and _Pl_ denotes the target location. +| \\E[ _Pc_ ; _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ $ x +: DECFRA +: VT420 +: Fill rectangular area. _Pc_ is the character to use, _Pt_, _Pl_, + _Pb_ and _Pr_ denotes the rectangle. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ $ z +: DECERA +: VT400 +: Erase rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ denotes the + rectangle. +| \\E[ _Ps_ T +: SD +: VT420 +: Scroll down _Ps_ lines. +| \\E[ s +: SCOSC +: SCO, VT510 +: Save cursor position. +| \\E[ u +: SCORC +: SCO, VT510 +: Restore cursor position. +| \\E[ _Ps_ SP q +: DECSCUSR +: VT510 +: Set cursor style. In foot, _Ps_=0 means "use style from foot.ini". +| \\E[ = _Ps_ c +: DA3 +: VT510 +: Send tertiary device attributes. Foot responds with "FOOT", in + hexadecimal. +| \\E[ _Pm_ d +: VPA +: ECMA-48 +: Line position absolute - move cursor to line _Pm_. +| \\E[ _Pm_ e +: VPR +: ECMA-48 +: Line position relative - move cursor down _Pm_ lines. +| \\E[ _Pm_ a +: HPR +: ECMA-48 +: Character position relative - move cursor to the right _Pm_ times. +| \\E[ _Ps_ E +: CNL +: ECMA-48 +: Cursor next line - move the cursor down _Ps_ times. +| \\E[ _Ps_ F +: CPL +: ECMA-48 +: Cursor preceding line - move the cursor up _Ps_ times. +| \\E[ _Pm_ ` +: HPA +: ECMA-48 +: Character position absolute - move cursor to column _Pm_. +| \\E[ _Ps_ G +: CHA +: ECMA-48 +: Cursor character absolute - move cursor to column _Ps_. + cursor. _Ps_=2 -> all. _Ps_=3 -> saved lines. +| \\E[ _Ps_ S +: SU +: ECMA-48 +: Scroll up _Ps_ lines. +| \\E[ _Ps_ I +: CHT +: ECMA-48 +: Cursor forward tabulation _Ps_ tab stops. +| \\E[ _Ps_ Z +: CBT +: ECMA-48 +: Cursor backward tabulation _Ps_ tab stops. +| \\E[ _Ps_ b +: REP +: ECMA-48 +: Repeat the preceding printable character _Ps_ times. +| \\E[ ? _Pi_ ; _Pa_ ; _Pv_ S +: XTSMGRAPHICS +: xterm +: Set or request sixel attributes. +| \\E[ > _Ps_ q +: XTVERSION +: xterm +: _Ps_=0 -> report terminal name and version, in the form + *\\EP>|foot(version)\\E\\*. +| \\E[ > 4 ; _Pv_ m +: XTMODKEYS +: xterm +: Set level of the _modifyOtherKeys_ property to _Pv_. Note that foot + only supports level 1 and 2, where level 1 is the default setting. +| \\E[ ? _Pp_ m +: XTQMODKEYS +: xterm +: Query key modifier options +| \\E[ > 4 n +: +: xterm +: Resets the _modifyOtherKeys_ property to level 1. Note that in foot, + this sequence does not completely disable _modifyOtherKeys_, since + foot only supports level 1 and level 2 (and not level 0). +| \\E[ ? u +: +: kitty +: Query current values of the Kitty keyboard flags. +| \\E[ > _flags_ u +: +: kitty +: Push a new entry, _flags_, to the Kitty keyboard stack. +| \\E[ < _number_ u +: +: kitty +: Pop _number_ of entries from the Kitty keyboard stack. +| \\E[ = _flags_ ; _mode_ u +: +: kitty +: Update current Kitty keyboard flags, according to _mode_. +| \\E[ # P +: XTPUSHCOLORS +: xterm +: Push current color palette onto stack +| \\E[ # Q +: XTPOPCOLORS +: xterm +: Pop color palette from stack +| \\E[ # R +: XTREPORTCOLORS +: xterm +: Report the current entry on the palette stack, and the number of + palettes stored on the stack. +| \\E[ ? 996 n +: Query the current (color) theme mode +: contour +: The current color theme mode (light or dark) is reported as *CSI ? + 997 ; 1|2 n*, where *1* means dark and *2* light. By convention, the + primary theme in foot is considered dark, and the alternative theme + light. + + +# OSC + +All _OSC_ sequences begin with *\\E]*, sometimes abbreviated _OSC_. + +[[ *Sequence* +:[ *Origin* +:< *Description* +| \\E] 0 ; _Pt_ \\E\\ +: xterm +: Set window icon and title to _Pt_. +| \\E] 1 ; _Pt_ \\E\\ +: xterm +: Set window icon to _Pt_. +| \\E] 2 ; _Pt_ \\E\\ +: xterm +: Set window title to _Pt_ +| \\E] 4 ; _c_ ; _spec_ \\E\\ +: xterm +: Change color number _c_ to _spec_, where _spec_ is a color in + XParseColor format. foot only supports RGB colors; either + *rgb://*, or the legacy format (*#rgb*). +| \\E] 7 ; _Uri_ \\E\\ +: iTerm2 +: Update the terminal's current working directory. Newly spawned + terminals will launch in this directory. _Uri_ must be in the format + *file:///*. *hostname* must refer to your local host. +| \\E] 8 ; id=_ID_ ; _Uri_ \\E\\ +: VTE+iTerm2 +: Hyperlink (a.k.a HTML-like anchors). id=_ID_ is optional; if assigned, + all URIs with the same _ID_ will be treated as a single + hyperlink. An empty URI closes the hyperlink. +| \\E] 9 ; _msg_ \\E\\ +: iTerm2 +: Desktop notification, uses *notify* in *foot.ini*(5). +| \\E] 10 ; _spec_ \\E\\ +: xterm +: Change the default foreground color to _spec_, a color in + XParseColor format. +| \\E] 11 ; _spec_ \\E\\ +: xterm +: Change the default background color to _spec_, a color in + XParseColor format. Foot implements URxvt's transparency extension; + e.g. _spec_=*[75]#ff00ff* or _spec_=*rgba:ff/00/ff/bf* (pink with + 75% alpha). +| \\E] 12 ; _spec_ \\E\\ +: xterm +: Change cursor color to _spec_, a color in XParseColor format. +| \\E] 17 ; _spec_ \\E\\ +: xterm +: Change selection background color to _spec_, a color in + XParseColor format. +| \\E] 19 ; _spec_ \\E\\ +: xterm +: Change selection foreground color to _spec_, a color in XParseColor + format. +| \\E] 22 ; _xcursor-pointer-name_ \\E\\ +: xterm +: Sets the xcursor pointer. An empty name, or an invalid name resets + it. +| \\E] 52 ; _Pc_ ; ? \\E\\ +: xterm +: Send clipboard data. _Pc_ can be either *c*, *s* or *p*. *c* uses + the clipboard as source, and *s* and *p* uses the primary + selection. The response is *\\E] 52 ; Pc ; + \E\\*, where _Pc_ denotes the source used. +| \\E] 52 ; _Pc_ ; _Pd_ \\E\\ +: xterm +: Copy _Pd_ (base64 encoded text) to the clipboard. _Pc_ denotes the + target: *c* targets the clipboard and *s* and *p* the primary + selection. +| \\E] 66 ; _params_ ; text \\E\\ +: kitty +: Text sizing protocol (only 'w', width, supported) +| \\E] 99 ; _params_ ; _payload_ \\E\\ +: kitty +: Desktop notification; uses *desktop-notifications.command* in + *foot.ini*(5). +| \\E] 104 ; _c_ \\E\\ +: xterm +: Reset color number _c_ (multiple semicolon separated _c_ values may + be provided), or all colors (excluding the default + foreground/background colors) if _c_ is omitted. +| \\E] 110 \\E\\ +: xterm +: Reset default foreground color +| \\E] 111 \\E\\ +: xterm +: Reset default background color +| \\E] 112 \\E\\ +: xterm +: Reset cursor color +| \\E] 117 \\E\\ +: xterm +: Reset selection background color +| \\E] 119 \\E\\ +: xterm +: Reset selection foreground color +| \\E] 133 ; A \\E\\ +: FinalTerm +: Mark start of shell prompt +| \\E] 133 ; C \\E\\ +: FinalTerm +: Mark start of command output +| \\E] 133 ; D \\E\\ +: FinalTerm +: Mark end of command output +| \\E] 176 ; _app-id_ \\E\\ +: foot +: Set app ID. _app-id_ is optional; if assigned, + the terminal window App ID will be set to the value. + An empty App ID resets the value to the default. +| \\E] 555 \\E\\ +: foot +: Flash the entire terminal (foot extension) +| \\E] 777;notify;_title_;_msg_ \\E\\ +: urxvt +: Desktop notification, uses *desktop-notifications.command* in + *foot.ini*(5). + +# DCS + +All _DCS_ sequences begin with *\\EP* (sometimes abbreviated _DCS_), +and are terminated by *\\E\\* (ST). + +[[ *Sequence* +:< *Description* +| \\EP q \\E\\ +: Emit a sixel image at the current cursor position +| \\EP $ q \\E\\ +: Request selection or setting (DECRQSS). Implemented queries: + DECSTBM, SGR and DECSCUSR. +| \\EP = _C_ s \\E\\ +: Begin (_C_=*1*) or end (_C_=*2*) application synchronized updates. + This sequence is supported for compatibility reasons, but it's + recommended to use private mode 2026 (see above) instead. +| \\EP + q \\E\\ +: Query builtin terminfo database (XTGETTCAP) + + +# FOOTNOTE + +Foot does not support 8-bit control characters ("C1"). diff --git a/doc/foot.1.scd b/doc/foot.1.scd new file mode 100644 index 0000000..a190db9 --- /dev/null +++ b/doc/foot.1.scd @@ -0,0 +1,735 @@ +foot(1) + +# NAME + +foot - Wayland terminal emulator + +# SYNOPSIS + +*foot* [_OPTIONS_]++ +*foot* [_OPTIONS_] <_command_> [_COMMAND OPTIONS_] + +All trailing (non-option) arguments are treated as a command, and its +arguments, to execute (instead of the default shell). + +# DESCRIPTION + +*foot* is a Wayland terminal emulator. Running it without arguments +will start a new terminal window with your default shell. + +You can override the default shell by appending a custom command to +the foot command line + + *foot htop* + +# OPTIONS + +*-c*,*--config*=_PATH_ + Path to configuration file, see *foot.ini*(5) for details. + + The configuration file is automatically passed to new terminals + spawned via *spawn-terminal* (see *foot.ini*(5)). + +*-C*,*--check-config* + Verify configuration and then exit with 0 if ok, otherwise exit + with 230 (see *EXIT STATUS*). + +*-o*,*--override*=[_SECTION_.]_KEY_=_VALUE_ + Override an option set in the configuration file. If _SECTION_ is not + given, defaults to _main_. + +*-f*,*--font*=_FONT_ + Comma separated list of fonts to use, in fontconfig format (see + *FONT FORMAT*). + + The first font is the primary font. The remaining fonts are + fallback fonts that will be used whenever a glyph cannot be found + in the primary font. + + The fallback fonts are searched in the order they appear. If a + glyph cannot be found in any of the fallback fonts, the dynamic + fallback list from fontconfig (for the primary font) is + searched. + + Default: _monospace_. + +*-w*,*--window-size-pixels*=_WIDTHxHEIGHT_ + Set initial window width and height, in pixels. Default: _700x500_. + +*-W*,*--window-size-chars*=_WIDTHxHEIGHT_ + Set initial window width and height, in characters. Default: _not set_. + +*-t*,*--term*=_TERM_ + Value to set the environment variable *TERM* to (see *TERMINFO* + and *ENVIRONMENT*). Default: _@default_terminfo@_. + +*-T*,*--title*=_TITLE_ + Initial window title. Default: _foot_. + +*-a*,*--app-id*=_ID_ + Value to set the *app-id* property on the Wayland window + to. Default: _foot_ (normal mode), or _footclient_ (server mode). + +*toplevel-tag*=_TAG_ + Value to set the *toplevel-tag* property on the Wayland window + to. The compositor can use this value for session management, + window rules etc. Default: _not set_ + +*-m*,*--maximized* + Start in maximized mode. If both *--maximized* and *--fullscreen* + are specified, the _last_ one takes precedence. + +*-F*,*--fullscreen* + Start in fullscreen mode. If both *--maximized* and *--fullscreen* + are specified, the _last_ one takes precedence. + +*-L*,*--login-shell* + Start a login shell, by prepending a '-' to argv[0]. + +*--pty* + Display an existing pty instead of creating one. This is useful + for interacting with VM consoles. + + This option is not currently supported in combination with + *-s*,*--server*. + +*-D*,*--working-directory*=_DIR_ + Initial working directory for the client application. Default: + _CWD of foot_. + +*-s*,*--server*[=_PATH_|_FD_] + Run as a server. In this mode, a single foot instance hosts + multiple terminals (windows). Use *footclient*(1) to launch new + terminals. + + This saves some memory since for example fonts and glyph caches + can be shared between the terminals. + + It also saves upstart time since the config has already been + loaded and parsed, and most importantly, fonts have already been + loaded (and their glyph caches are likely to already have been + populated). + + Each terminal will have its own rendering threads, but all Wayland + communication, as well as input/output to the shell, is + multiplexed in the main thread. Thus, this mode might result in + slightly worse performance when multiple terminals are under heavy + load. + + Also be aware that should one terminal crash, it will take all the + others with it. + + The default path is + *$XDG\_RUNTIME\_DIR/foot-$WAYLAND\_DISPLAY.sock*. + + If *$XDG\_RUNTIME\_DIR* is not set, the default path is instead + */tmp/foot.sock*. + + If *$XDG\_RUNTIME\_DIR* is set, but *$WAYLAND\_DISPLAY* is not, + the default path is *$XDG\_RUNTIME\_DIR/foot.sock*. + + Note that if you change the default, you will also need to use the + *--server-socket* option in *footclient*(1) and point it to your + custom socket path. + + If the argument is a number, foot will interpret it as the file descriptor + of a socket provided by a supervision daemon (such as systemd or s6), and + use that socket as it's own. + + Two systemd units (foot-server.{service,socket}) are provided to use that + feature with systemd. To use socket activation, only enable the + socket unit. + + Note that starting *foot --server* as a systemd service will use + the environment of the systemd user instance; thus, you'll need + to import *$WAYLAND_DISPLAY* in it using *systemctl --user + import-environment WAYLAND_DISPLAY*. + +*-H*,*--hold* + Remain open after child process exits. + +*-p*,*--print-pid*=_FILE_|_FD_ + Print PID to this file, or FD, when successfully started. The file + (or FD) is closed immediately after writing the PID. When a _FILE_ + as been specified, the file is unlinked at exit. + + This option can only be used in combination with *-s*,*--server*. + +*-d*,*--log-level*={*info*,*warning*,*error*,*none*} + Log level, used both for log output on stderr as well as + syslog. Default: _warning_. + +*-l*,*--log-colorize*=[{*never*,*always*,*auto*}] + Enables or disables colorization of log output on stderr. Default: + _auto_. + +*-S*,*--log-no-syslog* + Disables syslog logging. Logging is only done on stderr. This + option can only be used in combination with *-s*,*--server*. + +*-v*,*--version* + Show the version number and quit. + +*-e* + Ignored; for compatibility with *xterm -e*. + + This option was added in response to several program launchers + passing *-e* to arbitrary terminals, under the assumption that + they all implement the same semantics for it as *xterm*(1). + Ignoring it allows foot to be invoked as e.g. *foot -e man foot* + with the same results as with xterm, instead of producing an + "invalid option" error. + +# KEYBOARD SHORTCUTS + +The following keyboard shortcuts are available by default. They can be +changed in *foot.ini*(5). There are also more actions (disabled by +default) available; see *foot.ini*(5). + +## NORMAL MODE + +*shift*+*page up*/*page down* + Scroll up/down in history + +*ctrl*+*shift*+*c*, *XF86Copy* + Copy selected text to the _clipboard_ + +*ctrl*+*shift*+*v*, *XF86Paste* + Paste from _clipboard_ + +*shift*+*insert* + Paste from the _primary selection_ + +*ctrl*+*shift*+*r* + Start a scrollback search + +*ctrl*+*+*, *ctrl*+*=* + Increase font size + +*ctrl*+*-* + Decrease font size + +*ctrl*+*0* + Reset font size + +*ctrl*+*shift*+*n* + Spawn a new terminal. If the shell has been configured to emit the + _OSC 7_ escape sequence, the new terminal will start in the + current working directory. + +*ctrl*+*shift*+*o* + Activate URL mode, allowing you to "launch" URLs. + +*ctrl*+*shift*+*u* + Activate Unicode input. + +*ctrl*+*shift*+*z* + Jump to the previous, currently not visible, prompt. Requires + shell integration. + +*ctrl*+*shift*+*x* + Jump to the next prompt. Requires shell integration. + +## SCROLLBACK SEARCH + +These keyboard shortcuts affect the search selection: + +*ctrl*+*r* + Search _backward_ for the next match. If the search string is + empty, the last searched-for string is used. + +*ctrl*+*s* + Search _forward_ for the next match. If the search string is + empty, the last searched-for string is used. + +*shift*+*right* + Extend current selection to the right by one character. + +*shift*+*left* + Extend current selection to the left by one character. + +*ctrl*+*w*, *ctrl*+*shift*+*right* + Extend current selection (and thus the search criteria) to the end + of the word, or the next word if currently at a word separating + character. + +*ctrl*+*shift*+*w* + Same as *ctrl*+*w*, except that the only word separating + characters are whitespace characters. + +*ctrl*+*shift*+*left* + Extend current selection to the left to the last word boundary. + +*shift*+*down* + Extend current selection down one line + +*shift*+*up* + Extend current selection up one line. + +*ctrl*+*v*, *ctrl*+*shift*+*v*, *ctrl*+*y*, *XF86Paste* + Paste from clipboard into the search buffer. + +*shift*+*insert* + Paste from primary selection into the search buffer. + +*escape*, *ctrl*+*g*, *ctrl*+*c* + Cancel the search + +*return* + Finish the search and copy the current match to the primary + selection. The terminal selection is kept, allowing you to press + *ctrl*+*shift*+*c* to copy it to the clipboard. + +These shortcuts affect the search box in scrollback-search mode: + +*ctrl*+*b* + Moves the cursor in the search box one **character** to the left. + +*ctrl*+*left*, *alt*+*b* + Moves the cursor in the search box one **word** to the left. + +*ctrl*+*f* + Moves the cursor in the search box one **character** to the right. + +*ctrl*+*right*, *alt*+*f* + Moves the cursor in the search box one **word** to the right. + +*Home*, *ctrl*+*a* + Moves the cursor in the search box to the beginning of the input. + +*End*, *ctrl*+*e* + Moves the cursor in the search box to the end of the input. + +*alt*+*backspace*, *ctrl*+*backspace* + Deletes the **word before** the cursor. + +*alt*+*delete*, *ctrl*+*delete* + Deletes the **word after** the cursor. + +*ctrl*+*u* + Deletes from the cursor to the start of the input + +*ctrl*+*k* + Deletes from the cursor to the end of the input + +These shortcuts affect scrolling in scrollback-search mode: + +*shift*+*page-up* + Scrolls up/back one page in history. + +*shift*+*page-down* + Scroll down/forward one page in history. + +## URL MODE + +*t* + Toggle URL visibility in jump label. + +*escape*, *ctrl*+*g*, *ctrl*+*c*, *ctrl*+*d* + Exit URL mode without launching a URL. + +## MOUSE SHORTCUTS + +*left*, single-click + Drag to select; when released, the selected text is copied to the + _primary_ selection. This feature is normally *disabled* whenever + the client has enabled _mouse tracking_, but can be forced by + holding *shift*. + + Holding *ctrl* will create a block selection. + +*left*, double-click + Selects the _word_ (separated by spaces, period, comma, + parenthesis etc) under the pointer. Hold *ctrl* to select + everything under the pointer up to, and until, the next space + characters. + +*left*, triple-click + Selects the everything between enclosing quotes, or the entire row + if not inside a quote. + +*left*, quad-click + Selects the entire row + +*middle* + Paste from the _primary_ selection + +*right* + Extend current selection. Clicking immediately extends the + selection, while hold-and-drag allows you to interactively resize + the selection. + +*ctrl*+*right* + Extend the current selection, but force it to be character wise, + rather than depending on the original selection mode. + +*wheel* + Scroll up/down in history + +*ctrl*+*wheel* + Increase/decrease font size + +## TOUCHSCREEN + +*tap* + Emulates mouse left button click. + +*drag* + Scrolls up/down in history. + + Holding for a while before dragging (time delay can be configured) + emulates mouse dragging with left button held. + + +# FONT FORMAT + +The font is specified in FontConfig syntax. That is, a colon-separated +list of font name and font options. + +_Examples_: +- Dina:weight=bold:slant=italic +- Courier New:size=12 + +# URLs + +Foot supports URL detection. But, unlike many other terminal +emulators, where URLs are highlighted when they are hovered and opened +by clicking on them, foot uses a keyboard driven approach. + +Pressing *ctrl*+*shift*+*o* enters _"Open URL mode"_, where all currently +visible URLs are underlined, and is associated with a +_"jump-label"_. The jump-label indicates the _key sequence_ +(e.g. *"AF"*) to use to activate the URL. + +The key binding can, of course, be customized, like all other key +bindings in foot. See *show-urls-launch* and *show-urls-copy* in +*foot.ini*(5). + +*show-urls-launch* by default opens the URL with *xdg-open*. This can +be changed with the *url-launch* option. + +*show-urls-copy* is an alternative to *show-urls-launch*, that changes +what activating a URL _does_; instead of opening it, it copies it to +the clipboard. It is unbound by default. + +Jump label colors, the URL underline color, and the letters used in +the jump label key sequences can be configured. + +# ALT/META CHARACTERS + +By default, foot prefixes meta characters with *ESC*. This corresponds +to XTerm's *metaSendsEscape* option set to *true*. + +This can be disabled programmatically with *\E[?1036l* (and enabled +again with *\E[?1036h*). + +When disabled, foot will instead set the 8:th bit of meta character +and then UTF-8 encode it. This corresponds to XTerm's *eightBitMeta* +option set to *true*. + +This can also be disabled programmatically with *rmm* (Reset Meta Mode, +*\E[?1034l*), and enabled again with *smm* (Set Meta Mode, +*\E[?1034h*). + +# BACKSPACE + +Foot transmits DEL (*^?*) on backspace. This corresponds to XTerm's +*backarrowKey* option set to *false*, and to DECBKM being _reset_. + +To instead transmit BS (*^H*), press *ctrl*+*backspace*. + +Note that foot does *not* implement DECBKM, and that the behavior +described above *cannot* be changed. + +Finally, pressing *alt* will prefix the transmitted byte with ESC. + +# KEYPAD + +By default, *Num Lock* overrides the run-time configuration keypad +mode; when active, the keypad is always considered to be in +_numerical_ mode. This corresponds to XTerm's *numLock* option set to +*true*. + +In this mode, the keypad keys always sends either numbers (Num Lock is +active) or cursor movement keys (up, down, left, right, page up, page +down etc). + +This can be disabled programmatically with *\E[?1035l* (and enabled +again with *\E[?1035h*). + +When disabled, the keypad sends custom escape sequences instead of +numbers, when in _application_ mode. + +# CONFIGURATION + +foot will search for a configuration file in the following locations, +in this order: + + - *XDG_CONFIG_HOME/foot/foot.ini* (defaulting to + *$HOME/.config/foot/foot.ini* if unset) + - *XDG_CONFIG_DIRS/foot/foot.ini* (defaulting to + */etc/xdg/foot/foot.ini* if unset) + +An example configuration file containing all options with their default value +commented out will usually be installed to */etc/xdg/foot/foot.ini*. + +For more information, see *foot.ini*(5). + +# SHELL INTEGRATION + +## Current working directory + +New foot terminal instances (bound to *ctrl*+*shift*+*n* by default) +will open in the current working directory, if the shell in the +"parent" terminal reports directory changes. + +This is done with the OSC-7 escape sequence. Most shells can be +scripted to do this, if they do not support it natively. See the wiki +(https://codeberg.org/dnkl/foot/wiki#user-content-spawning-new-terminal-instances-in-the-current-working-directory) +for details. + + +## Jumping between prompts + +Foot can move the current viewport to focus prompts of already +executed commands (bound to *ctrl*+*shift*+*z*/*x* by default). + +For this to work, the shell needs to emit an OSC-133;A +(*\\E]133;A\\E\\\\*) sequence before each prompt. + +In zsh, one way to do this is to add a _precmd_ hook: + + *precmd() { + print -Pn "\\e]133;A\\e\\\\" + }* + +See the wiki +(https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts) +for details, and examples for other shells. + +## Piping last command's output + +The key binding *pipe-command-output* can pipe the last command's +output to an application of your choice (similar to the other +*pipe-\** key bindings): + + *\[key-bindings\]++ +pipe-command-output=[sh -c "f=$(mktemp); cat - > $f; footclient emacsclient -nw $f; rm $f"] Control+Shift+g* + +When pressing *ctrl*+*shift*+*g*, the last command's output is written +to a temporary file, then an emacsclient is started in a new +footclient instance. The temporary file is removed after the +footclient instance has closed. + +For this to work, the shell must emit an OSC-133;C (*\\E]133;C\\E\\\\*) +sequence before command output starts, and an OSC-133;D +(*\\E]133;D\\E\\\\*) when the command output ends. + +In fish, one way to do this is to add _preexec_ and _postexec_ hooks: + + *function foot_cmd_start --on-event fish_preexec + echo -en "\\e]133;C\\e\\\\" + end* + + *function foot_cmd_end --on-event fish_postexec + echo -en "\\e]133;D\\e\\\\" + end* + +See the wiki +(https://codeberg.org/dnkl/foot/wiki#user-content-piping-last-commands-output) +for details, and examples for other shells + +# TERMINFO + +Client applications use the terminfo identifier specified by the +environment variable *TERM* (set by foot) to determine terminal +capabilities. + +Foot has two terminfo definitions: *foot* and *foot-direct*, with +*foot* being the default. + +The difference between the two is in the number of colors they +describe; *foot* describes 256 colors and *foot-direct* 16.7 million +colors (24-bit truecolor). + +Note that using the *foot* terminfo does not limit the number of +usable colors to 256; applications can still use 24-bit RGB colors. In +fact, most applications work best with *foot* (including 24-bit +colors). Using *\*-direct* terminfo entries has been known to crash +some ncurses applications even. + +There are however applications that need a *\*-direct* terminfo entry +for 24-bit support. Emacs is one such example. + +While using either *foot* or *foot-direct* is strongly recommended, it +is possible to use e.g. *xterm-256color* as well. This can be useful +when remoting to a system where foot's terminfo entries cannot easily +be installed. + +Note that terminfo entries can be installed in the user's home +directory. I.e. if you do not have root access, or if there is no +distro package for foot's terminfo entries, you can install foot's +terminfo entries manually, by copying *foot* and *foot-direct* to +*~/.terminfo/f/*. + +# XTGETTCAP + +*XTGETTCAP* is an escape sequence initially introduced by XTerm, and +also implemented (and extended, to some degree) by Kitty. + +It allows querying the terminal for terminfo classic, file-based, +terminfo definition. For example, if all applications used this +feature, you would no longer have to install foot's terminfo on remote +hosts you SSH into. + +XTerm's implementation (as of XTerm-370) only supports querying key +(as in keyboard keys) capabilities, and three custom capabilities: + +- TN - terminal name +- Co - number of colors (alias for the colors capability) +- RGB - number of bits per color channel (different semantics from + the RGB capability in file-based terminfo definitions!). + +Kitty has extended this, and also supports querying all integer and +string capabilities. + +Foot supports this, and extends it even further, to also include +boolean capabilities. This means foot's entire terminfo can be queried +via *XTGETTCAP*. + +Note that both Kitty and foot handles responses to multi-capability +queries slightly differently, compared to XTerm. + +XTerm will send a single DCS reply, with ;-separated +capability/value pairs. There are a couple of issues with this: + +- The success/fail flag in the beginning of the response is always 1 + (success), unless the very first queried capability is invalid. +- XTerm will not respond at all to an invalid capability, unless it's + the first one in the XTGETTCAP query. +- XTerm will end the response at the first invalid capability. + +In other words, if you send a large multi-capability query, you will +only get responses up to, but not including, the first invalid +capability. All subsequent capabilities will be dropped. + +Kitty and foot on the other hand, send one DCS response for each +capability in the multi query. This allows us to send a proper +success/fail flag for each queried capability. Responses for all +queried capabilities are always sent. No queries are ever dropped. + +# EXIT STATUS + +Foot will exit with code 230 if there is a failure in foot itself. + +In all other cases, the exit code is that of the client application +(i.e. the shell). + +# ENVIRONMENT + +## Variables used by foot + +*SHELL* + The default child process to run, when no _command_ argument is + specified and the *shell* option in *foot.ini*(5) is not set. + +*HOME* + Used to determine the location of the configuration file, see + *foot.ini*(5) for details. + +*XDG\_CONFIG\_HOME* + Used to determine the location of the configuration file, see + *foot.ini*(5) for details. + +*XDG\_CONFIG\_DIRS* + Used to determine the location of the configuration file, see + *foot.ini*(5) for details. + +*XDG\_RUNTIME\_DIR* + Used to construct the default _PATH_ for the *--server* + option, when no explicit argument is given (see above). + +*WAYLAND\_DISPLAY* + Used to construct the default _PATH_ for the *--server* + option, when no explicit argument is given (see above). + +*XCURSOR\_THEME* + The name of the *Xcursor*(3) theme to use for pointers (typically + set by the Wayland compositor). + +*XCURSOR\_SIZE* + The size to use for *Xcursor*(3) pointers (typically set by the + Wayland compositor). + +## Variables set in the child process + +*TERM* + terminfo/termcap identifier. This is used by client applications + to determine which capabilities a terminal supports. The value is + set according to either the *--term* command-line option or the + *term* config option in *foot.ini*(5). + +*COLORTERM* + This variable is set to *truecolor*, to indicate to client + applications that 24-bit RGB colors are supported. + +*PWD* + Current working directory (at the time of launching foot) + +*SHELL* + Set to the launched shell, if the shell is valid (it is listed in + */etc/shells*). + +In addition to the variables listed above, custom environment +variables may be defined in *foot.ini*(5). + +## Variables *unset* in the child process + +*TERM_PROGRAM* +*TERM_PROGRAM_VERSION* + These environment variables are set by certain other terminal + emulators. We unset them, to prevent applications from + misdetecting foot. + +In addition to the variables listed above, custom environment +variables to unset may be defined in *foot.ini*(5). + +# Signals + +The following signals have special meaning in foot: + +- SIGUSR1: switch to the dark color theme (*[colors-dark]*). +- SIGUSR2: switch to the light color theme (*[colors-light]*). + +Note: you can send SIGUSR1/SIGUSR2 to a *foot --server* process too, +in which case all client instances will switch theme. Furthermore, all +future client instances will also use the selected theme. + +You can also send SIGUSR1/SIGUSR2 to a footclient instance, see +*footclient*(1) for details. + + +# BUGS + +Please report bugs to https://codeberg.org/dnkl/foot/issues + +Before you open a new issue, please search existing bug reports, both +open and closed ones. Chances are someone else has already reported +the same issue. + +The report should contain the following: + +- Foot version (*foot --version*). +- Log output from foot (run *foot -d info* from another terminal). +- Which Wayland compositor (and version) you are running. +- If reporting a crash, please try to provide a *bt full* backtrace + with symbols. +- Steps to reproduce. The more details the better. + +# IRC + +\#foot on irc.libera.chat + +# SEE ALSO + +*foot.ini*(5), *footclient*(1) diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd new file mode 100644 index 0000000..66daaea --- /dev/null +++ b/doc/foot.ini.5.scd @@ -0,0 +1,2166 @@ +foot.ini(5) + +# NAME + +foot.ini - configuration file for *foot*(1) + +# DESCRIPTION + +*foot* uses the standard _unix configuration format_, with section based +key/value pairs. The default section is usually unnamed, i.e. not prefixed +with a _[section]_. However it can also be explicitly named _[main]_, +say if it needs to be reopened after any of the other sections. + +foot will search for a configuration file in the following locations, +in this order: + + - *XDG_CONFIG_HOME/foot/foot.ini* (defaulting to + *$HOME/.config/foot/foot.ini* if unset) + - *XDG_CONFIG_DIRS/foot/foot.ini* (defaulting to + */etc/xdg/foot/foot.ini* if unset) + +An example configuration file containing all options with their default value +commented out will usually be installed to */etc/xdg/foot/foot.ini*. + +Options are set using KEY=VALUE pairs: + + *\[colors-dark\]*++ +*background=000000*++ +*foreground=ffffff* + +Empty values (*KEY=*) are not supported. String options do allow the +empty string to be set, but it must be quoted: *KEY=""* + +# SECTION: main + +*shell* + Executable to launch. Typically a shell. Default: _$SHELL_ if set, + otherwise the user's default shell (as specified in + _/etc/passwd_). You can also pass arguments. For example + */bin/bash --norc*. + +*login-shell* + Boolean. If enabled, the shell will be launched as a login shell, + by prepending a '-' to argv[0]. Default: _no_. + +*term* + Value to set the environment variable *TERM* to. Default: + _@default_terminfo@_ + +*font*, *font-bold*, *font-italic*, *font-bold-italic* + Comma separated list of fonts to use, in fontconfig format. That + is, a font name followed by a list of colon-separated + options. Most noteworthy is *:size=n* (or *:pixelsize=n*), which + is used to set the font size. Note that the font size is also + affected by the *dpi-aware* option. + + Examples: + - Dina:weight=bold:slant=italic + - Courier New:size=12 + - Fantasque Sans Mono:fontfeatures=ss01 + - Iosevka:fontfeatures=cv01=1:fontfeatures=cv06=1 + - Meslo LG S:size=12, Noto Color Emoji:size=12 + - Courier New:pixelsize=8 + + Be aware that, depending on your setup, there may be global + FontConfig options that overrides options set here. If an option + appears to have no effect, ensure there is no global configuration + file that sets the same option with *assign* or *assign_replace*; + use one of the many *append* or possibly *prepend* modes. + + For each option, the first font is the primary font. The remaining + fonts are fallback fonts that will be used whenever a glyph cannot + be found in the primary font. + + The fallback fonts are searched in the order they appear. If a + glyph cannot be found in any of the fallback fonts, the dynamic + fallback list from fontconfig (for the primary font) is + searched. + + *font-bold*, *font-italic* and *font-bold-italic* allow custom + fonts to be used for bold/italic/bold+italic fonts. If left + unconfigured, the bold/italic variants of the regular font(s) + specified in *font* are used. *Note*: you _may_ have to tweak the + size(s) of the custom bold/italic fonts to match the regular font. + + To disable bold and/or italic fonts, set e.g. *font-bold* to + _exactly_ the same value as *font*. + + **size** is in _points_ (as defined by the FontConfig format). To + set a _pixel_ size, use **pixelsize** instead. Note that pixel + sizes are unaffected by DPI aware rendering (see *dpi-aware*), but + are affected by desktop scaling. + + Default: _monospace:size=8_ (*font*), _not set_ (*font-bold*, + *font-italic*, *font-bold-italic*). + +*font-size-adjustment* + Amount, in _points_, _pixels_ or _percent_, to increment/decrement + the font size when zooming in or out. + + Examples: + ``` + font-size-adjustment=0.5 # Adjust by 0.5 points + font-size-adjustment=10px # Adjust by 10 pixels + font-size-adjustment=7.5% # Adjust by 7.5 percent + ``` + + Default: _0.5_ + +*include* + Absolute path to configuration file to import. + + The import file has its own section scope. I.e. the including + configuration is still in the default section after the include, + regardless of which section the included file ends in. + + - The path must be an absolute path, or start with *~/*. + - Multiple include directives are allowed, but only one path per + directive. + - Nested imports are allowed. + + Default: _not set_. + +*line-height* + An absolute value, in _points_, that override line height from the + font metrics. + + You can specify a height in _pixels_ by using the *px* suffix: + e.g. *line-height=12px*. + + *Warning*: when changing the font size at runtime (i.e. zooming in + or out), foot will change the line height by the same + percentage. However, due to rounding, it is possible the line + height will be "too small" for some font sizes, causing + e.g. underscores to "disappear". + + See also: *vertical-letter-offset*. + + Default: _not set_. + +*letter-spacing* + Spacing between letters, in _points_. A positive value will + increase the cell size, and a negative value shrinks it. + + You can specify a letter spacing in _pixels_ by using the *px* + suffix: e.g. *letter-spacing=2px*. + + See also: *horizontal-letter-offset*. + + Default: _0_. + +*horizontal-letter-offset*, *vertical-letter-offset* + Configure the horizontal and vertical offsets used when + positioning glyphs within cells, in _points_, relative to the top + left corner. + + To specify an offset in _pixels_, append *px*: + e.g. *horizontal-letter-offset=2px*. + + Default: _0_. + +*underline-offset* + Use a custom offset for underlines. The offset is, by default, in + _points_ and relative to the font's baseline. A positive value + positions the underline under the baseline, while a negative value + positions it above the baseline. + + To specify an offset in _pixels_, append *px*: + *underline-offset=2px*. + + If left unset (the default), the offset specified in the font is + used, or estimated by foot if the font lacks underline positioning + information. + + Default: _unset_. + +*underline-thickness* + Use a custom thickness (height) for underlines. The thickness is, by + default, in _points_. + + To specify a thickness in _pixels_, append *px*: + *underline-thickness=1px*. + + If left unset (the default), the thickness specified in the font is + used. + + Default: _unset_ + +*strikeout-thickness* + Use a custom thickness (height) for strikeouts. The thickness is, by + default, in _points_. + + To specify a thickness in _pixels_, append *px*: + *strikeout-thickness=1px*. + + If left unset (the default), the thickness specified in the font is + used. + + Default: _unset_ + +*gamma-correct-blending* + Boolean. When enabled, foot will do gamma-correct blending in + linear color space. This is how font glyphs are supposed to be + rendered, but since nearly no applications or toolkits are doing + it on Linux, the result may not look like you are used to. + + Compared to the default (disabled), bright glyphs on a dark + background will appear thicker, and dark glyphs on a light + background will appear thinner. + + FreeType can limit the effect of the latter, with a technique + called stem darkening. It is only available for CFF fonts + (OpenType, .otf) and disabled by default (in FreeType). You can + enable it by setting the environment variable + *FREETYPE_PROPERTIES="cff:no-stem-darkening=0"* before starting + foot. + + Also be aware that many fonts have been developed on systems that + do not do gamma-correct blending, and may therefore look thicker + than intended when rendered with gamma-correct blending, since the + font designer set the font weight based on incorrect rendering. + + In order to represent colors faithfully, higher precision image + buffers are required. By default, foot will use either 16-bit, or + 10-bit color channels, depending on availability, when + gamma-correct blending is enabled. However, the high precision + buffers are slow; if you want to use gamma-correct blending, but + prefer speed (throughput and input latency) over accurate colors, + you can force 8-bit color channels by setting + *tweak.surface-bit-depth=8-bit*. + + Default: _no_. + +*uppercase-regex-insert* + Boolean. When enabled, inputting an uppercase hint character in + *show-urls-copy* or *regex-copy* mode will insert the selected + text into the prompt in addition to copying it to the clipboard. + + Default: _yes_ + +*box-drawings-uses-font-glyphs* + Boolean. When disabled, foot generates box/line drawing characters + itself. There are several advantages to doing this instead of using + font glyphs: + + - No antialiasing effects where e.g. line endpoints appear + dimmed down, or blurred. + - Line- and box characters are guaranteed to span the entire cell, + resulting in a gap-less appearance. + - No alignment issues, i.e. lines are centered when they should be. + - Many fonts lack some, or all, of the line- and box drawing + characters, causing fallback fonts to be used, which results + in out-of-place looking glyphs (for example, badly sized). + + When enabled, box/line drawing characters are rendered using font + glyphs. This may result in a more uniform look, in some use cases. + + When disabled, foot will render the following Unicode codepoints + by itself: + + - U+02500 - U+0259F + - U+02800 - U+028FF + - U+1CD00 - U+1CDE5 + - U+1Fb00 - U+1FB9B + + Default: _no_. + +*dpi-aware* + Boolean. + + When set to *yes*, fonts are sized using the monitor's DPI, making + a font of a given size have the same physical size, regardless of + monitor. In other words, if you drag a foot window between + different monitors, the font size remains the same. + + In this mode, the monitor's scaling factor is ignored; doubling + the scaling factor will *not* double the font size. + + When set to *no*, the monitor's DPI is ignored. The font is + instead sized using the monitor's scaling factor; doubling the + scaling factor *does* double the font size. + + Note that this option typically does not work with bitmap fonts, + which only contain a pre-defined set of sizes, and cannot be + dynamically scaled. Whichever size (of the available ones) that + best matches the DPI or scaling factor, will be used. + + Also note that if the font size has been specified in pixels + (*:pixelsize=*_N_, instead of *:size=*_N_), DPI scaling + (*dpi-aware=yes*) will have no effect (the specified pixel size + will be used as is). But, if the monitor's scaling factor is used + to size the font (*dpi-aware=no*), the font's pixel size will be + multiplied with the scaling factor. + + Default: _no_ + +*pad* + Padding between border and glyphs, in pixels (subject to output + scaling), in the form + + ``` + _XxY_ [center | center-when-fullscreen | center-when-maximized-and-fullscreen] + ``` + or + ``` + RIGHTxTOPxLEFTxBOTTOM [center | center-when-fullscreen | center-when-maximized-and-fullscreen] + ``` + + - `_XxY_` adds _at least_: + - X pixels on the left and right sides. + - Y pixels on the top and bottom sides. + + - `LEFTxTOPxRIGHTxBOTTOM` adds **at least**: + - LEFT pixels to the left + - TOP pixels to the top + - RIGHT pixels to the right + - BOTTOM pixels to the bottom + + When no centering is specified, the grid content is anchored to + the top left corner. I.e. if the window manager forces an odd + window size on foot, the additional pixels will be added to the + right and bottom sides. + + If *center* is specified, the grid content is instead + centered. This may cause "jumpiness" when resizing the window. + + With *center-when-fullscreen* and + *center-when-maximized-and-fullscreen*, the grid is anchored to + the top left corner, unless the window is maximized, or + fullscreened. + + Default: _0x0_ center-when-maximized-and-fullscreen. + +*resize-delay-ms* + + Time, in milliseconds, of "idle time" before foot performs text + reflow, and sends the new window dimensions to the client + application while doing an interactive resize of a foot + window. Idle time in this context is a period of time where the + window size is not changing. + + In other words, while you are fiddling with the window size, foot + does not send the updated dimensions to the client. It also does a + fast "truncating" resize of the grid, instead of actually + reflowing the contents. Only when you pause the fiddling for + *resize-delay-ms* milliseconds is the client updated, and the + contents properly reflowed. + + Emphasis is on _while_ here; as soon as the interactive resize + ends (i.e. when you let go of the window border), the final + dimensions are sent to the client, without any delays. + + Setting it to 0 disables the delay completely. + + Default: _100_. + +*resize-by-cells* + Boolean. + + When set to *yes*, the window size will be constrained to multiples + of the cell size (plus any configured padding). When set to *no*, + the window size will be unconstrained, and padding may be adjusted + as necessary to accommodate window sizes that are not multiples of + the cell size. + + This option only applies to floating windows. Sizes of maximized, tiled + or fullscreen windows will not be constrained to multiples of the cell + size. + + Default: _yes_ + +*resize-keep-grid* + Boolean. + + When set to *yes*, the window size will be adjusted with changes in font + size to preserve the dimensions of the text grid. When set to *no*, the + window size will remain constant and the text grid will be adjusted as + necessary to fit the window. + + This option only applies to floating windows. + + Default: _yes_ + +*initial-color-theme* + Selects which color theme to use, *dark*, or *light*. + + *dark* uses the colors defined in the *colors-dark* section, while + *light* uses the colors from the *colors-light* section. + + Use the *color-theme-switch-dark*, *color-theme-switch-light* and + *color-theme-toggle* key bindings to switch between the two themes + at runtime, or send SIGUSR1/SIGUSR2 to the foot process (see + *foot*(1) for details). + + Default: _dark_ + +*initial-window-size-pixels* + Initial window width and height in _pixels_ (subject to output + scaling), in the form _WIDTHxHEIGHT_. The height _includes_ the + titlebar when using CSDs. Mutually exclusive to + *initial-window-size-chars*. + + Note that this option may not work as expected if fractional + scaling is being used, due to the fact that many compositors do + not report the correct scaling factor until after a window has + been mapped. + + Default: _700x500_. + +*initial-window-size-chars* + Initial window width and height in _characters_, in the form + _WIDTHxHEIGHT_. Mutually exclusive to + *initial-window-size-pixels*. + + Note that if you have a multi-monitor setup, with different + scaling factors, there is a possibility the window size will not + be set correctly. If that is the case, use + *initial-window-size-pixels* instead. + + And, just like *initial-window-size-pixels*, this option may not + work as expected if fractional scaling is being used (see + *initial-window-size-pixels* for details). + + Default: _not set_. + +*initial-window-mode* + Initial window mode for each newly spawned window: *windowed*, + *maximized* or *fullscreen*. Default: _windowed_. + +*title* + Initial window title. Default: _foot_. + +*locked-title* + Boolean. If enabled, applications are not allowed to change the + title at run-time. Default: _no_. + +*app-id* + Value to set the *app-id* property on the Wayland window to. The + compositor can use this value to e.g. group multiple windows, or + apply window management rules. Default: _foot_ (normal mode), or + _footclient_ (server mode). + +*toplevel-tag* + Value to set the *toplevel-tag* property on the Wayland window + to. The compositor can use this value for session management, + window rules etc. Default: _not set_ + +*bold-text-in-bright* + Semi-boolean. When enabled, bold text is rendered in a brighter + color (in addition to using a bold font). The color is brightened + by blending it with white. + + If set to *palette-based*, rather than a simple *yes|true*, colors + matching one of the 8 regular palette colors will be brightened + using the corresponding bright palette color. Other colors will + not be brightened. + + Default: _no_. + +*word-delimiters* + String of characters that act as word delimiters when selecting + text. Note that whitespace characters are _always_ word + delimiters, regardless of this setting. Default: _,│`|:"'()[]{}<>_ + +*selection-target* + Clipboard target to automatically copy selected text to. One of + *none*, *primary*, *clipboard* or *both*. Default: _primary_. + +*workers* + Number of threads to use for rendering. Set to 0 to disable + multithreading. Default: the number of available logical CPUs + (including SMT). Note that this is not always the best value. In + some cases, the number of physical _cores_ is better. + + In case you have a ridiculous amount of cores and/or threads, + consider limiting the number of *workers*, since foot cannot + parallelize more than the number of visible rows. + +*utmp-helper* + Path to utmp logging helper binary. + + When starting foot, an utmp record is created by launching the + helper binary with the following arguments: + + ``` + @utmp_add_args@ + ``` + + When foot is closed, the utmp record is removed by launching the + helper binary with the following arguments: + + ``` + @utmp_del_args@ + ``` + + Set to *none* to disable utmp records. Default: _@utmp_helper_path@_. + +# SECTION: environment + +This section is used to define environment variables that will be set +in the client application, in addition to the variables inherited from +the terminal process itself. + +The format is simply: + +*name*=_value_ + +Note: do not set *TERM* here; use the *term* option in the main +(default) section instead. + +# SECTION: security + +*osc52* + + Whether OSC-52 (clipboard access) is enabled or disabled. One of + *disabled*, *copy-enabled*, *paste-enabled* or *enabled*. + + OSC-52 gives terminal application access to the host clipboard + (i.e. the Wayland clipboard). This is normally not a security + issue, since all applications can access the clipboard directly + over the Wayland socket. + + However, when SSH:ing into a remote system, or accessing a + container etc, the terminal applications may be untrusted, and you + might consider disabling the host clipboard access. + + - *disabled*: disables all clipboard access + - *copy-enabled*: applications can write to the clipboard, but not + read from it. + - *paste-enabled*: applications can read from the clipboard, but + not write to it. + - *enabled*: all applications have full access to the host + clipboard. This is the default. + + Default: _enabled_ + + +# SECTION: bell + +*system* + Boolean, when set to _yes_, ring the system bell. The bell is rung + independent of whether the foot window has keyboard focus or + not. Exact behavior is compositor dependent. + + Default: _yes_ + +*urgent* + Boolean, when set to _yes_, foot will signal urgency to the + compositor through the XDG activation protocol whenever *BEL* is + received, and the window does NOT have keyboard focus. + + If the compositor does not implement this protocol, the margins + will be painted in red instead. + + Applications can enable/disable this feature programmatically with + the *CSI ? 1042 h* and *CSI ? 1042 l* escape sequences. + + Default: _no_ + +*notify* + Boolean, when set to _yes_, foot will emit a desktop notification + using the command specified in the *notify* option whenever *BEL* + is received. By default, bell notifications are shown only when + the window does *not* have keyboard focus. See + _desktop-notifications.inhibit-when-focused_. + + Default: _no_ + +*visual* + Boolean, when set to _yes_, foot will flash the terminal + window. Default: _no_ + +*command* + When set, foot will execute this command when *BEL* is received. + Default: none + +*command-focused* + Boolean, whether to run the command on *BEL* even while + focused. Default: _no_ + +# SECTION: desktop-notifications + +*command* + Command to execute to display a notification. + + Template arguments + _${title}_ and _${body}_ will be replaced with the + notification's actual _title_ and _body_ (message content). + + _${app-id}_ is replaced with the value of the command line + option _--app-id_, and defaults to *foot* (normal mode), or + *footclient* (server mode). + + _${window-title}_ is replaced with the current window title. + + _${icon}_ is replaced by the icon specified in the + notification request, or the empty string if no icon was + specified. Can be used with e.g. notify-send's *--icon* + option, or preferably, by setting the *image-path* hint (with + e.g. notify-send's *--hint* option). + + _${category}_ is replaced by the notification's category. Can + be used together with e.g. notify-send's *--category* option. + + _${urgency}_ is replaced with the notifications urgency; + *low*, *normal* or *critical*. Can be used together with + e.g. notify-send's *--urgency* option. + + _${expire-time}_ is replaced with the notification specified + notification timeout. Can be used together with + e.g. notify-send's *--expire-time* option. + + _${replace-id}_ is replaced by the notification daemon + assigned ID that the notification replaces/updates. For this + to work, foot needs to know the externally assigned IDs of + previously emitted notifications, see the 'stdout' section + below. Can be used together with e.g. notify-send's + *--replace-id* option. + + _${muted}_ is replaced by either *true* or *false*, depending + on whether the notification has requested all notification + sounds be muted. It is intended to set the *suppress-sound* + hint (with e.g. notify-send's *--hint* option). + + _${sound-name}_ is replaced by sound-name requested by the + notification. This should be a name from the freedesktop sound + naming specification, but this is not something that foot + enforces. It is intended to set the *sound-name* hint (with + e.g. notify-send's *--hint* option). + + _${action-argument}_ will be expanded to the + *command-action-argument* option, for each notification + action. There will always be at least one action, the + "default" action. Foot uses this to enable window focusing, + and reporting notification activation to applications that + requested such events. + + Applications can also define their own custom notification + actions. See the *command-action-argument* option for details. + + Ways to trigger notifications + Applications can trigger notifications in the following ways: + + - OSC 777: *\\e]777;notify;;<body>\\e\\\\* + - OSC 99: *\\e]99;;<title>\\e\\\\* (this is just a bare bones + example; this protocol has lots of features, see + https://sw.kovidgoyal.net/kitty/desktop-notifications) + + By default, notifications are *inhibited* if the foot window + has keyboard focus. See + _desktop-notifications.inhibit-when-focused_. + + Window activation (focusing) + Foot can focus the window when the notification is + 'activated'. It can also send an event back to the client + application, notifying it that the notification has been + 'activated', This typically happens when the default action is + invoked, and/or when the notification is clicked, but exact + behavior depends on the notification daemon in use, and how it + has been configured. + + For this to work, foot needs to know when the notification was + activated (as opposed to just dismissed), and it needs an XDG + activation token. + + There are two parts to handle this. First, the notification + must define an action. For this purpose, foot will add a + "default" action to the notification (see the + *command-action-argument* option). + + Second, foot needs to know when the notification is activated, + and it needs to get hold of the XDG activation token. + + Both are expected to be printed on stdout. + + Foot expects the action name (not label) to be printed on a + single line. No prefix, no postfix. + + Foot expects the activation token to be printed on a single + line, prefixed with *xdgtoken=*. + + Example: + default++ +xdgtoken=18179adf579a7a904ce73754964b1ec3 + + The expected format of stdout may change at any time. Please + read the changelog when upgrading foot. + + *Note*: notify-send does not, out of the box, support + reporting the XDG activation token in any way. This means + window activation will not work by default. + + Stdout + Foot recognizes the following things from the notification + helper's stdout: + + - _id_: integer in base 10, daemon assigned notification ID + - *id=*_id_: same as plain _nnn_. + - *default*: the 'default' action was triggered + - *action=*_default_: same as _default_ + - *action=*_n_: application custom action _n_ triggered + - _n_: integer in base 10, appearing after the ID; application + custom action _n_ triggered + - *xdgtoken=*_xyz_: XDG activation token. + + Example #1: + 17++ +action=default++ +xdgtoken=95ebdfe56e4f47ddb5bba9d7dc3a2c35 + + Foot recognizes this as: + - notification has the daemon assigned ID 17 + - the user triggered the default action + - the notification sent an XDG activation token + + Example #2: + 17++ +1 + + Foot recognizes this as: + - notification has the daemon assigned ID 17 + - the user triggered the first custom action, "1" + + Example #3: + id=17++ +1 + + Foot recognizes this as: + - notification has the daemon assigned ID 17 + - the user triggered the first custom action, "1" + + Default: _notify-send++ + --wait++ + --app-name ${app-id}++ + --icon ${app-id}++ + --category ${category}++ + --urgency ${urgency}++ + --expire-time ${expire-time}++ + --hint STRING:image-path:${icon}++ + --hint BOOLEAN:suppress-sound:${muted}++ + --hint STRING:sound-name:${sound-name}++ + --replace-id ${replace-id}++ + ${action-argument}++ + --print-id++ + -- ${title} ${body}_. + +*command-action-argument* + String to use with *command* to enable passing action/button names + to the notification helper. + + Foot will always configure a "default" action that can be used to + "activate" the notification, which in turn can cause the foot + window to be focused, or an escape to be sent to the terminal + application (depending on how the application generated the + notification). + + Furthermore, the OSC-99 notifications protocol allows applications + to define their own actions. Foot uses a combination of the + *command* option, and the *command-action-argument* option to pass + the names of the actions to the notification helper. + + This option has the following template arguments: + + - _${action-name}_: the name of the action; *default* for the + default action configured by foot, and _n_, where _n_ is an + integer >= 1, for application defined actions. + - _${action-label}_: *Activate* for the default action, and a + free-form string for application defined actions. + + For each notification action (remember, there will always be at + least one), *command-action-argument* will be expanded with the + action's name and label. + + Then, _${action-argument}_ is expanded in *command* to the full list + of actions. + + If *command-action-argument* is set to the empty string, no + actions will be passed to *command*. That is, _${action-argument}_ + will be replaced with the empty string. + + Example: + + *command-action-argument=--action ${action-name}=${action-label}*++ +*command=notify-send ${action-argument} ...* + + Assume the application defined two custom actions: *OK* and + *Cancel*. + + Given the above, foot will execute: + + notify-send++ + --action default='Click to activate'++ + --action 1=OK++ + --action 2=Cancel++ + ... + + Default: _--action ${action-name}=${action-label}_ + +*close* + Command to execute to close an existing notification. + + _${id}_ is expanded to the ID of the notification that should be + closed. For example: + + fyi --close ${id} + + Closing a notification is only supported by the Kitty Desktop + Notification protocol, OSC-99. + + If set to the empty string (the default), foot will instead try to + close the notification by sending SIGINT to the notification + helper process. For example, *notify-send --wait* (libnotify >= + 0.8.0) responds to SIGINT by closing the notification. + + Default: _not set_ + +*inhibit-when-focused* + Boolean. If enabled, foot will not display notifications if the + terminal window has keyboard focus. + + Default: _yes_ + +# SECTION: scrollback + +*lines* + Number of scrollback lines. The maximum number of allocated lines + will be this value plus the number of visible lines, rounded up to + the nearest power of 2. Default: _1000_. + +*multiplier* + Amount to multiply mouse scrolling with. It is a decimal number, + i.e. fractions are allowed. Default: _3.0_. + +*indicator-position* + Configures the style of the scrollback position indicator. One of + *none*, *fixed* or *relative*. *none* disables the indicator + completely. *fixed* always renders the indicator near the top of + the window, and *relative* renders the indicator at the position + corresponding to the current scrollback position. Default: + _relative_. + +*indicator-format* + Which format to use when displaying the scrollback position + indicator. Either _percentage_, _line_, or a custom fixed + string. This option is ignored if + *indicator-position=none*. Default: _empty string_. + +# SECTION: url + +Note that you can also add custom regular expressions, see the 'regex' +section. + +*launch* + Command to execute when opening URLs. _${url}_ will be replaced + with the actual URL. Default: _xdg-open ${url}_. + +*osc8-underline* + When to underline OSC-8 URLs. Possible values are *url-mode* and + *always*. + + When set to *url-mode*, OSC-8 URLs are only highlighted in URL + mode, just like auto-detected URLs. + + When set to *always*, OSC-8 URLs are always highlighted, + regardless of their other attributes (bold, italic etc). Note that + this does _not_ make them clickable. + + Default: _url-mode_ + +*style* + The underline style to use when rendering URL underlines. This + applies to both OSC-8 underlines when *osc8-underline=always*, and + all detected URLs in URL mode. One of *none*, *single*, *double*, + *curly*, *dotted* or *dashed*. + + Default: _dotted_ + +*label-letters* + String of characters to use when generating key sequences for URL + jump labels. + + If you change this option to include the letter *t*, you should + also change the default *[url-bindings].toggle-url-visible* key + binding to avoid a clash. + + Default: _sadfjklewcmpgh_. + +*regex* + Regular expression to use when auto-detecting URLs. The format is + "POSIX-Extended Regular Expressions". Note that the first marked + subexpression is used as the URL. In other words, if you want the + whole regex match to be used as an URL, surround all of it with + parenthesis: *(regex-pattern)*. + + Default: _(((https?://|mailto:|ftp://|file:|ssh:|ssh://|git://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:)|www\.)([0-9a-zA-Z:/?#@!$&\*+,;=.~\_%^\-]+|\([]\["0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&\*+,;=.~\_%^\-]\*')+([0-9a-zA-Z/#@$&\*+=~\_%^\-]|\([]\["0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'\*+,;=.~\_%^\-]\*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&\*+,;=.~\_%^\-]\*'))_ + +# SECTION: regex + +Similar to the 'url' mode, but with custom defined regular expressions +(and launchers). + +To use a custom defined regular expression, you also need to add a key +binding for it. This is done in the *key-binding* section, see below +for details. For example, a regex to detect hash digests (e.g. git +commit hashes) could look like: + +``` +[regex:hashes] +regex=([a-fA-F0-9]{7,128}) +launch=path-to-script-or-application ${match} + +[key-bindings] +regex-launch=[hashes] Control+Shift+q +regex-copy=[hashes] Control+Mod1+Shift+q +``` + +*launch* + Command to execute when "launching" a regex match. _${match}_ will + be replaced with the actual URL. Default: _not set_. + +*regex* + Regular expression to use when matching text. The format is + "POSIX-Extended Regular Expressions". Note that the first marked + subexpression is used as the match. In other words, if you want + the whole regex match to be used, surround all of it with + parenthesis: *(regex-pattern)*. + + Default: _not set_. + + +# SECTION: cursor + +This section controls the cursor style and color. Note that +applications can change these at runtime. + +*style* + Configures the default cursor style, and is one of: *block*, + *beam*, *underline* or *hollow*. Note that this can be overridden + by applications. Default: _block_. + +*unfocused-style* + Configures how the cursor is rendered when the terminal window is + unfocused. Possible values are: + + - unchanged: render cursor in exactly the same way as when the + window has focus. + - hollow: render a block cursor, but hollowed out. + - none: do not display any cursor at all. + +*blink* + Boolean. Enables blinking cursor. Note that this can be overridden + by applications. Related option: *blink-rate*. Default: _no_. + +*blink-rate* + The rate at which the cursor blinks, when cursor blinking has been + enabled. Expressed in milliseconds between each blink. Default: + _500_. + +*beam-thickness* + Thickness (width) of the beam styled cursor. The value is in + points, and its exact value thus depends on the monitor's DPI. To + instead specify a thickness in pixels, use the *px* suffix: + e.g. *beam-thickness=2px*. Default: _1.5_ + +*underline-thickness* + Thickness (height) of the underline styled cursor. The value is in + points, and its exact value thus depends on the monitor's DPI. + + To instead specify a thickness in pixels, use the *px* suffix: + e.g. *underline-thickness=2px*. + + Note that if left unset, the cursor's thickness will scale with + the font size, while if set, the size is fixed. + + Default: _font underline thickness_. + +# SECTION: mouse + +*hide-when-typing* + Boolean. When enabled, the mouse cursor is hidden while + typing. Default: _no_. + +*alternate-scroll-mode* + Boolean. This option controls the initial value for the _alternate + scroll mode_. When this mode is enabled, mouse scroll events are + translated to _up_/_down_ key events when displaying the alternate + screen. + + This lets you scroll with the mouse in e.g. pagers (like _less_) + without enabling native mouse support in them. + + Alternate scrolling is *not* used if the application enables + native mouse support. + + This option can be modified by applications at run-time using the + escape sequences *CSI ? 1007 h* (enable) and *CSI ? 1007 l* + (disable). + + Default: _yes_. + +# SECTION: touch + +*long-press-delay* + Number of milliseconds to distinguish between a short press and + a long press on the touchscreen. + + Default: _400_. + +# SECTION: colors-dark, colors-light + +These two sections controls the 16 ANSI colors, the default foreground +and background colors, and the extended 256 color palette. Note that +applications can change these at runtime. + +The colors are in RRGGBB format (i.e. plain old 6-digit hex values, +without prefix). That is, they do *not* have an alpha component. You +can configure the background transparency with the _alpha_ option. + +*colors-dark* is intended to define a dark color theme, and +*colors-light* is intended to define a light color theme. You can +switch between them using the *color-theme-switch-dark*, +*color-theme-switch-light* and *color-theme-toggle* key bindings, or +by sending SIGUSR1/SIGUSR2 to the foot process. + +The default theme used is *colors-dark*, unless +*initial-color-theme=light* has been set. + +*cursor* + Two space separated RRGGBB values (i.e. plain old 6-digit hex + values, without prefix) specifying the foreground (text) and + background (cursor) colors for the cursor. + + Example: *ff0000 00ff00* (green cursor, red text) + + Default: the regular foreground and background colors, reversed. + +*foreground* + Default foreground color. This is the color used when no ANSI + color is being used. Default: _839496_. + +*background* + Default background color. This is the color used when no ANSI + color is being used. Default: _002b36_. + +*regular0*, *regular1* *..* *regular7* + The eight basic ANSI colors (Black, Red, Green, Yellow, Blue, + Magenta, Cyan, White). Default: _242424_, _f62b5a_, _47b413_, + _e3c401_, _24acd4_, _f2affd_, _13c299_, _e6e6e6_ (starlight + theme, V4). + +*bright0*, *bright1* *..* *bright7* + The eight bright ANSI colors (Black, Red, Green, Yellow, Blue, + Magenta, Cyan, White). Default: _616161_, _ff4d51_, _35d450_, + _e9e836_, _5dc5f8_, _feabf2_, _24dfc4_, _ffffff_ (starlight + theme, V4). + +*dim0*, *dim1* *..* *dim7* + Custom colors to use with dimmed colors. Dimmed colors do not have + an entry in the color palette. Applications emit them by combining + a color value, and a "dim" attribute. + + By default, foot implements this by blending the current color + with black or white, depending on what the *dim-blend-towards* + option is set to . This is a generic approach that applies to both + colors from the 256-color palette, as well as 24-bit RGB colors. + + You can change this behavior by setting the *dimN* options. When + set, foot will match the current color against the color palette, + and if it matches one of the *regularN* colors, the corresponding + *dimN* color will be used. + + If instead the current color matches one of the *brightN* colors, + the corresponding *regularN* color will be used. + + If the current color does not match any known color, it is dimmed + by blending with black (i.e. the same behavior as if the *dimN* + options are unconfigured). 24-bit RGB colors will typically fall + into this category. + + Note that applications can change the *regularN* and *brightN* + colors at runtime. However, they have no way of changing the + *dimN* colors. If an application has changed the *regularN* + colors, foot will still use the corresponding *dimN* color, as + configured in foot.ini. + + Default: _not set_. + +*0* *..* *255* + Arbitrary colors in the 256-color palette. Default: for *0* *..* + *15*, see regular and bright defaults above; see + https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit for an + explanation of the remainder. + +*sixel0* *..* *sixel15* + The default sixel color palette. Default: _000000_, _3333cc_, + _cc2121_, _33cc33_, _cc33cc_, _33cccc_, _cccc33_, _878787_, + _424242_, _545499_, _994242_, _549954_, _995499_, _549999_, + _999954_, _cccccc_. + +*alpha* + Background translucency. A value in the range 0.0-1.0, where 0.0 + means completely transparent, and 1.0 is opaque. Default: _1.0_. + +*alpha-mode* + Specifies when *alpha* is applied. One of *default*, *matching* or + *all*. + + *default* applies *alpha* to cells with the default background + color, excluding cells with the same RGB value as the default + background color. + + *matching* is the same as *default*, but also applies *alpha* to + cells with the same RGB value as the default background color. + + *all* applies *alpha* to all cells, regardless of background color. + + Default: _default_ + +*blur* + Boolean. When enabled, foot will blur the background (main window + only, not CSDs etc), when it is transparent. This feature requires + the compositor to implement the _ext-background-effect-v1_ + protocol (and specifically, the _blur_ effect). + + Default: _no_ + +*dim-blend-towards* + Which color to blend towards when "auto" dimming a color (see + *dim0*..*dim7* above). One of *black* or *white*. Blending towards + black makes the text darker, while blending towards white makes it + whiter (but still dimmer than normal text). + + Default: _black_ (*colors-dark*), _white_ (*colors-light*) + +*selection-foreground*, *selection-background* + Foreground (text) and background color to use in selected + text. Default: _inverse foreground/background_. + +*jump-labels* + Two color values specifying the foreground (text) and background + colors to use when rendering jump labels in URL mode. Default: + _regular0 regular3_. + +*scrollback-indicator* + Two color values specifying the foreground (text) and background + (indicator itself) colors for the scrollback indicator. Default: + _regular0 bright4_. + +*search-box-no-match* + Two color values specifying the foreground (text) and background + colors for the scrollback search box, when there are no + matches. Default: _regular0 regular1_. + +*search-box-match* + Two color values specifying the foreground (text) and background + colors for the scrollback search box, when the search box is + either empty, or there are matches. Default: _regular0 regular3_. + +*urls* + Color to use for the underline used to highlight URLs in URL + mode. Default: _regular3_. + +*flash* + Color to use for the terminal window flash. Default: _7f7f00_. + +*flash-alpha* + Flash translucency. A value in the range 0.0-1.0, where 0.0 means + completely transparent, and 1.0 is opaque. Default: _0.5_. + +# SECTION: csd + +This section controls the look of the _CSDs_ (Client Side +Decorations). Note that the default is to *not* use CSDs, but instead +to use _SSDs_ (Server Side Decorations) when the compositor supports +it. + +Note that unlike the colors defined in the _colors_ section, the color +values here are in AARRGGBB (i.e. plain old 8-digit hex values) +format. I.e. they contain an alpha component - 00 means completely +transparent, and ff fully opaque. + +Examples: + +- ffffffff: white, fully opaque +- ff000000: black, fully opaque +- 7fffffff: white, semi-transparent +- ff00ff00: green, fully opaque + +*preferred* + Which type of window decorations to prefer: *client* (CSD), + *server* (SSD) or *none*. + + Note that this is only a hint to the compositor. Depending on + compositor support, and how it has been configured, it may + instruct foot to use CSDs even though this option has been set to + *server*, or render SSDs despite *client* or *none* being set. + + Default: _server_. + +*size* + Height, in pixels (subject to output scaling), of the + titlebar. Setting it to 0 will hide the titlebar, while still + showing the border (if *border-width* is set to a non-zero + value). Default: _26_. + +*color* + Titlebar color. Default: use the default _foreground_ color. + +*font* + Font to use for the title bar. This is a list of fonts, similar to + the main *font* option. Note that the font will be sized using the + title bar size. That is, all *:size* and *:pixelsize* attributes + will be ignored. Default: _primary font_. + +*hide-when-maximized* + Boolean. When enabled, the CSD titlebar is hidden when the window + is maximized. To completely disable the titlebar, set *size* to 0 + instead. Default: _no_. + +*double-click-to-maximize* + Boolean. When enabled, double-clicking the CSD titlebar will + (un)maximize the window. Default: _yes_. + +*border-width* + Width of the border, in pixels (subject to output scaling). Note + that the border encompasses the entire window, including the title + bar. Default: _0_. + +*border-color* + Color of border. By default, the title bar color is used. If the + title bar color has not been set, the default foreground color + (from the color scheme) is used. Default: _titlebar color_. + +*button-width* + Width, in pixels (subject to output scaling), of the + minimize/maximize/close buttons. Default: _26_. + +*button-color* + Foreground color on the minimize/maximize/close buttons and the + titlebar text. Default: use the default _background_ color. + +*button-minimize-color* + Minimize button's background color. Default: use the default + _regular4_ color (blue). + +*button-maximize-color* + Maximize button's background color. Default: use the default + _regular2_ color (green). + +*button-close-color* + Close button's background color. Default: use the default + _regular1_ color (red). + +# SECTION: key-bindings + +This section lets you override the default key bindings. + +The general format is _action=combo1...comboN_. That is, each action +may have one or more key combinations, space separated. Each +combination is in the form _mod1+mod2+key_. The names of the modifiers +and the key *must* be valid XKB key names. + +Note that if *Shift* is one of the modifiers, the _key_ *must not* be +in upper case. For example, *Control+Shift+V* will never trigger, but +*Control+Shift+v* will. + +The default key bindings all use "real" modifiers (*Mod1*, *Mod4* +etc), but "virtual" modifiers (*Alt*, *Super* etc) are allowed. + +*xkbcli interactive-wayland* can be useful for finding keysym names. + +When matching key presses to key bindings, foot uses a couple of +different approaches. + +As an example, let's say you press ctrl+shift+c (assume plain us ASCII +layout). XKB will tell foot *Control+C* was pressed. Note the lack of +the shift modifier, and the upper case 'C'. Internally, this is called +the "translated" form. + +The "untranslated" form (*Control+Shift+c*) is derived from the +translated form, and is what foot tries to match first. + +If no "untranslated" key bindings can be found, foot proceeds to +checking the "translated" variant. + +This means you can use either form in your foot configuration, and +that *Control+Shift+c* (and similar) has higher priority than +*Control+C*. Also note that while foot normally detects when the same +combination is assigned to multiple actions, it will not detect +*Control+C* vs. *Control+Shift+c* collisions. Call it a known bug... + +Finally, foot tries to match the raw key code. Here, the primary +layout is queried for all key codes that generate a particular XKB +symbol, and the pressed key's code is matched against this. For +example, if you use the layouts *"us,de(neo)"*, the 'r' key generates +the symbol 'c' in the neo layout. I.e. to get a 'c', you press +'r'. The match logic described above will only match 'c' key bindings +(e.g. *Control+Shift+c*). The raw mode however, will match 'r' key +bindings (e.g. *Control+Shift+r*). This is useful for non-latin +layouts, where you would otherwise have to customize all key bindings. + +A key combination can only be mapped to *one* action. Let's say you +want to bind *Control+Shift+R* to *fullscreen*. Since this is the +default shortcut for *search-start*, you first need to unmap the +default binding. This can be done by setting _action=none_; +e.g. *search-start=none*. + +*noop* + All key combinations listed here will not be sent to the + application. Default: _none_. + +*scrollback-up-page* + Scrolls up/back one page in history. Default: _Shift+Page\_Up + Shift+KP\_Page\_Up_. + +*scrollback-up-half-page* + Scrolls up/back half of a page in history. Default: _none_. + +*scrollback-up-line* + Scrolls up/back a single line in history. Default: _none_. + +*scrollback-down-page* + Scroll down/forward one page in history. Default: + _Shift+Page\_Down Shift+KP\_Page\_Down_. + +*scrollback-down-half-page* + Scroll down/forward half of a page in history. Default: _none_. + +*scrollback-down-line* + Scroll down/forward a single line in history. Default: _none_. + +*scrollback-home* + Scroll to the beginning of the scrollback. Default: _none_. + +*scrollback-end* + Scroll to the end (bottom) of the scrollback. Default: _none_. + +*clipboard-copy* + Copies the current selection into the _clipboard_. Default: _Control+Shift+c_ + _XF86Copy_. + +*clipboard-paste* + Pastes from the _clipboard_. Default: _Control+Shift+v_ _XF86Paste_. + +*primary-paste* + Pastes from the _primary selection_. Default: _Shift+Insert_ (also + defined in *mouse-bindings*). + +*search-start* + Starts a scrollback/history search. Default: _Control+Shift+r_. + +*font-increase* + Increases the font size by 0.5pt. Default: _Control+plus + Control+equal Control+KP\_Add_ (also defined in *mouse-bindings*). + +*font-decrease* + Decreases the font size by 0.5pt. Default: _Control+minus + Control+KP\_Subtract_ (also defined in *mouse-bindings*). + +*font-reset* + Resets the font size to the default. Default: _Control+0 Control+KP\_0_. + +*spawn-terminal* + Spawns a new terminal. If the shell has been configured to emit + the OSC 7 escape sequence, the new terminal will start in the + current working directory. Default: _Control+Shift+n_. + +*minimize* + Minimizes the window. Default: _none_. + +*maximize* + Toggle the maximized state. Default: _none_. + +*fullscreen* + Toggles the fullscreen state. Default: _none_. + +*pipe-visible*, *pipe-scrollback*, *pipe-selected*, *pipe-command-output* + Pipes the currently visible text, the entire scrollback, the + currently selected text, or the last command's output to an + external tool. The syntax for this option is a bit special; the + first part of the value is the command to execute enclosed in + "[]", followed by the binding(s). + + You can configure multiple pipes as long as the command strings + are different and the key bindings are unique. + + Note that the command is *not* automatically run inside a shell; + use *sh -c "command line"* if you need that. + + Example #1: + # Extract currently visible URLs, let user choose one (via + fuzzel), then launch firefox with the selected URL++ +*pipe-visible=[sh -c "xurls | uniq | tac | fuzzel | xargs -r firefox"] Control+Print* + + Example #2: + # Open scrollback contents in Emacs running in a new foot instance++ +*pipe-scrollback=[sh -c "f=$(mktemp) && cat - > $f && foot emacsclient -t $f; rm $f"] Control+Shift+Print* + + Default: _none_ + +*show-urls-launch* + Enter URL mode, where all currently visible URLs are tagged with a + jump label with a key sequence that will open the URL (and exit + URL mode). Default: _Control+Shift+o_. + +*show-urls-persistent* + Similar to *show-urls-launch*, but does not automatically exit URL + mode after activating an URL. Default: _none_. + +*show-urls-copy* + Enter URL mode, where all currently visible URLs are tagged with a + jump label with a key sequence that will place the URL in the + clipboard. If the hint is completed with an uppercase character, + the match will also be pasted. Default: _none_. + +*regex-launch* + Enter regex mode. This works exactly the same as URL mode; all + regex matches are tagged with a jump label with a key sequence + that will "launch" to match (and exit regex mode). + + The name of the regex section must be specified in the key + binding: + + ``` + [regex:hashes] + regex=([a-fA-F0-9]{7,128}) + launch=path-to-script-or-application ${match} + + [key-bindings] + regex-launch=[hashes] Control+Shift+q + regex-copy=[hashes] Control+Mod1+Shift+q + ``` + + Default: _none_. + +*regex-copy* + Same as *regex-launch*, but the match is placed in the clipboard, + instead of "launched", upon activation. If the hint is completed + with an uppercase character, the match will also be pasted. + Default: _none_. + +*prompt-prev* + Jump to the previous, currently not visible, prompt (requires + shell integration, see *foot*(1)). Default: _Control+Shift+z_. + +*prompt-next* + Jump to the next prompt (requires shell integration, see + *foot*(1)). Default: _Control+Shift+x_. + +*unicode-input* + Input a Unicode character by typing its codepoint in hexadecimal, + followed by *Enter* or *Space*. + + For example, to input the character _ö_ (LATIN SMALL LETTER O WITH + DIAERESIS, Unicode codepoint 0xf6), you would first activate this + key binding, then type: *f*, *6*, *Enter*. + + Another example: to input 😍 (SMILING FACE WITH HEART-SHAPED EYES, + Unicode codepoint 0x1f60d), activate this key binding, then type: + *1*, *f*, *6*, *0*, *d*, *Enter*. + + Recognized key bindings in Unicode input mode: + + - Enter, Space: commit the Unicode character, then exit this mode. + - Escape, q, Ctrl+c, Ctrl+d, Ctrl+g: abort input, then exit this mode. + - 0-9, a-f: append next digit to the Unicode's codepoint. + - Backspace: undo the last digit. + + Note that there is no visual feedback while in this mode. This is + by design; foot's Unicode input mode is considered to be a + fallback. The preferred way of entering Unicode characters, emojis + etc is by using an IME. + + Default: _Control+Shift+u_. + +*color-theme-switch-dark*, *color-theme-switch-light*, *color-theme-toggle* + Switch between the dark color theme (defined in the *colors-dark* + section), and the light color theme (defined in the *colors-light* + section). + + *color-theme-switch-dark* applies the dark color theme regardless + of which color theme is currently active. + + *color-theme-switch-light* applies the light color theme + regardless of which color theme is currently active. + + *color-theme-toggle* toggles between the primary and alternative + color themes. + + Note: you can also send SIGUSR1/SIGUSR2 to the foot process to + change the theme (see *foot*(1) for details.) + + Default: _none_ + +*quit* + Quit foot. Default: _none_. + +# SECTION: search-bindings + +This section lets you override the default key bindings used in +scrollback search mode. The syntax is exactly the same as the regular +**key-bindings**. + +*cancel* + Aborts the search. The viewport is restored and the _primary + selection_ is **not** updated. Default: _Control+g Control+c + Escape_. + +*commit* + Exit search mode and copy current selection into the _primary + selection_. Viewport is **not** restored. To copy the selection to + the regular _clipboard_, use *Control+Shift+c*. Default: _Return + KP_Enter_. + +*find-prev* + Search **backwards** in the scrollback history for the next + match. Default: _Control+r_. + +*find-next* + Searches **forwards** in the scrollback history for the next + match. Default: _Control+s_. + +*cursor-left* + Moves the cursor in the search box one **character** to the + left. Default: _Left Control+b_. + +*cursor-left-word* + Moves the cursor in the search box one **word** to the + left. Default: _Control+Left Mod1+b_. + +*cursor-right* + Moves the cursor in the search box one **character** to the + right. Default: _Right Control+f_. + +*cursor-right-word* + Moves the cursor in the search box one **word** to the + right. Default: _Control+Right Mod1+f_. + +*cursor-home* + Moves the cursor in the search box to the beginning of the + input. Default: _Home Control+a_. + +*cursor-end* + Moves the cursor in the search box to the end of the + input. Default: _End Control+e_. + +*delete-prev* + Deletes the **character before** the cursor. Default: _BackSpace_. + +*delete-prev-word* + Deletes the **word before** the cursor. Default: _Mod1+BackSpace + Control+BackSpace_. + +*delete-next* + Deletes the **character after** the cursor. Default: _Delete_. + +*delete-next-word* + Deletes the **word after** the cursor. Default: _Mod1+d + Control+Delete_. + +*delete-to-start* + Deletes search input before the cursor. Default: _Ctrl+u_. + +*delete-to-end* + Deletes search input after the cursor. Default: _Ctrl+k_. + +*extend-char* + Extend current selection to the right, by one character. Default: + _Shift+Right_. + +*extend-to-word-boundary* + Extend current selection to the right, to the next word + boundary. Default: _Control+w Control+Shift+Right_. + +*extend-to-next-whitespace* + Extend the current selection to the right, to the next + whitespace. Default: _Control+Shift+w_. + +*extend-line-down* + Extend current selection down one line. Default: _Shift+Down_. + +*extend-backward-char* + Extend current selection to the left, by one character. Default: + _Shift+Left_. + +*extend-backward-to-word-boundary* + Extend current selection to the left, to the next word + boundary. Default: _Control+Shift+Left_. + +*extend-backward-to-next-whitespace* + Extend the current selection to the left, to the next + whitespace. Default: _none_. + +*extend-line-up* + Extend current selection up one line. Default: _Shift+Up_. + +*clipboard-paste* + Paste from the _clipboard_ into the search buffer. Default: + _Control+v Control+y Control+Shift+v XF86Paste_. + +*primary-paste* + Paste from the _primary selection_ into the search + buffer. Default: _Shift+Insert_. + +*unicode-input* + Unicode input mode. See _key-bindings.unicode-input_ for + details. Default: _none_. + +*scrollback-up-page* + Scrolls up/back one page in history. Default: _Shift+Page\_Up + Shift+KP\_Page\_Up_. + +*scrollback-up-half-page* + Scrolls up/back half of a page in history. Default: _none_. + +*scrollback-up-line* + Scrolls up/back a single line in history. Default: _none_. + +*scrollback-down-page* + Scroll down/forward one page in history. Default: + _Shift+Page\_Down Shift+KP\_Page\_Down_. + +*scrollback-down-half-page* + Scroll down/forward half of a page in history. Default: _none_. + +*scrollback-down-line* + Scroll down/forward a single line in history. Default: _none_. + +*scrollback-home* + Scroll to the beginning of the scrollback. Default: _none_. + +*scrollback-end* + Scroll to the end (bottom) of the scrollback. Default: _none_. + +# SECTION: url-bindings + +This section lets you override the default key bindings used in URL +mode. The syntax is exactly the same as the regular **key-bindings**. + +Be careful; do not use single-letter keys that are also used in +*[url].label-letters*, as doing so will make some URLs inaccessible. + +*cancel* + Exits URL mode without opening a URL. Default: _Control+g + Control+c Control+d Escape_. + +*toggle-url-visible* + By default, the jump label only shows the key sequence required to + activate it. This is fine as long as the URL is visible in the + original text. + + But with e.g. OSC-8 URLs (the terminal version of HTML anchors, + i.e. "links"), the text on the screen can be something completely + different than the URL. + + This action toggles between showing and hiding the URL on the jump + label. + + Default: _t_. + +# SECTION: text-bindings + +This section lets you remap key combinations to custom escape +sequences. + +The format is _text=combo1...comboN_. That is, the string to emit may +have one or more key combinations, space separated. Each combination +is in the form _mod1+mod2+key_. The names of the modifiers and the key +*must* be valid XKB key names. + +The text string specifies the characters, or bytes, to emit when the +associated key combination(s) are pressed. There are two ways to +specify a character: + +- Normal, printable characters are written as-is: *abcdef*. +- Bytes (e.g. ESC) are written as two-digit hexadecimal numbers, with + a *\\x* prefix: *\\x1b*. + +Example: you would like to remap _Super+k_ to the _Up_ key. + +The escape sequence for the Up key is _ESC [ A_ (without the +spaces). Thus, we need to specify this in foot.ini (*Mod4* is the XKB +name for the Super/logo key): + +*\\x1b[A = Mod4+k* + +Another example: to remap _Super+c_ to _Control+c_: + +*\\x03 = Mod4+c* + +# SECTION: mouse-bindings + +This section lets you override the default mouse bindings. + +The general format is _action=combo1...comboN_. That is, each action +may have one or more key combinations, space separated. Each +combination is in the form _mod1+mod2+BTN\_<name>[-COUNT]_. The names +of the modifiers *must* be valid XKB key names, and the button name +*must* be a valid libinput name. You can find the button names using +*libinput debug-events*. + +The trailing *COUNT* (number of times the button has to be clicked) is +optional and specifies the click count required to trigger the +binding. The default if *COUNT* is omitted is _1_. + +To map wheel events (i.e. scrolling), use the button names +*BTN_WHEEL_BACK* (up) and *BTN_WHEEL_FORWARD* (down). Note that these +events never generate a *COUNT* larger than 1. That is, +*BTN_WHEEL_BACK+2*, for example, will never trigger. + +Foot also recognizes tiltable wheels; to map these, use +*BTN_WHEEL_LEFT* and *BTN_WHEEL_RIGHT*. + +A modifier+button combination can only be mapped to *one* action. Let's +say you want to bind *BTN\_MIDDLE* to *fullscreen*. Since +*BTN\_MIDDLE* is the default binding for *primary-paste*, you first +need to unmap the default binding. This can be done by setting +_action=none_; e.g. *primary-paste=none*. + +*selection-override-modifiers* + The modifiers set in this set (which may be set to any combination + of modifiers, e.g. _mod1+mod2+mod3_, as well as _none_) are used + to enable selecting text with the mouse irrespective of whether a + client application currently has the mouse grabbed. + These modifiers cannot be used as modifiers in mouse bindings. + Because the order of bindings is significant, it is best to set + this prior to any other mouse bindings that might use modifiers in + the default set. + Default: _Shift_ + +The actions to which mouse combos can be bound are listed below. All +actions listed under *key-bindings* can be used here as well. + +*scrollback-up-mouse* + Normal screen: scrolls up the contents. + + Alt screen: send fake _KeyUP_ events to the client application, if + alternate scroll mode is enabled. + + Default: _BTN\_WHEEL\_BACK_ + +*scrollback-down-mouse* + Normal screen: scrolls down the contents. + + Alt screen: send fake _KeyDOWN_ events to the client application, if + alternate scroll mode is enabled. + + Default: _BTN\_WHEEL\_FORWARD_ + +*select-begin* + Begin an interactive selection. The selection is finalized, and + copied to the _primary selection_, when the button is + released. Default: _BTN\_LEFT_. + +*select-begin-block* + Begin an interactive block selection. The selection is finalized, + and copied to the _primary selection_, when the button is + released. Default: _Control+BTN\_LEFT_. + +*select-word* + Begin an interactive word-wise selection, where words are + separated by whitespace and all characters defined by the + *word-delimiters* option. The selection is finalized, and copied + to the _primary selection_, when the button is released. Default: + _BTN\_LEFT-2_. + +*select-word-whitespace* + Same as *select-word*, but the characters in the *word-delimiters* + option are ignored. I.e only whitespace characters act as + delimiters. The selection is finalized, and copied to the _primary + selection_, when the button is released. Default: + _Control+BTN\_LEFT-2_. + +*select-quote* + Begin an interactive "quote" selection. This is similar to + *select-word*, except an entire quote is selected (that is, + everything inside the quote, excluding the quote + characters). Recognized quote characters are: *"* and *'*. + + If a complete quote cannot be found on the current logical row + (only one quote character, or none are found), the entire row is + selected. + + The selection is finalized, and copied to the _primary selection_, + when the button is released. + + After the initial selection has been made, it behaves like a + normal word, or row selection, depending on whether a quote was + found or not. This affects what happens when, for example, + extending the selection. + + Notes: + - Escaped quote characters are not supported (*"foo \\"bar"* will + match *'foo \\'*, not *'foo "bar'*). + - Foot does not try to handle mismatched quote characters; they + will simply not match. + - Nested quotes (using different quote characters) are supported. + + Default: _BTN\_LEFT-3_. + +*select-row* + Begin an interactive row-wise selection. The selection is + finalized, and copied to the _primary selection_, when the button + is released. Default: _BTN\_LEFT-4_. + +*select-extend* + Interactively extend an existing selection, using the original + selection mode (normal, block, word-wise or row-wise). The + selection is finalized, and copied to the _primary selection_, + when the button is released. Default: _BTN\_RIGHT_. + +*select-extend-character-wise* + Same as *select-extend*, but forces the selection mode to _normal_ + (i.e. character wise). Note that this causes subsequent + *select-extend* operations to be character wise. This action is + ignored for block selections. Default: _Control+BTN\_RIGHT_. + +*primary-paste* + Pastes from the _primary selection_. Default: _BTN\_MIDDLE_. + +*font-increase* + Increases the font size by 0.5pt. Default: + _Control+BTN\_WHEEL\_BACK_ (also defined in *key-bindings*). + +*font-decrease* + Decreases the font size by 0.5pt. Default: + _Control+BTN\_WHEEL\_FORWARD_ (also defined in *key-bindings*). + + +# TWEAK + +This section is for advanced users and describes configuration options +that can be used to tweak foot's low-level behavior. + +These options are *not* included in the example configuration. You +should not change these unless you understand what they do. + +Note that these options may change, or be removed at any time, without +prior notice. + +When reporting bugs, please mention if, and to what, you have changed +any of these options. + +*scaling-filter* + Overrides the default scaling filter used when down-scaling bitmap + fonts (e.g. emoji fonts). Possible values are *none*, *nearest*, + *bilinear*, *impulse*, *box*, *linear*, *cubic*, *gaussian*, + *lanczos2*, *lanczos3* or *lanczos3-stretched*. + + Default: _lanczos3_. + +*overflowing-glyphs* + Boolean. When enabled, glyphs wider than their cell(s) are allowed + to render into one additional neighbouring cell. + + One use case for this are fonts with wide italic characters that + "bend" into the next cell. Without this option, such glyphs will + appear "cut off". + + Another use case are fonts with "icon" characters in the Unicode + private usage area, e.g. Nerd Fonts, or Powerline Fonts and legacy + emoji characters like *WHITE FROWNING FACE*. + + Note: might impact performance depending on the font used. + Especially small font sizes can cause many overflowing glyphs + because of subpixel rendering. + + Default: _yes_. + +*render-timer* + Enables a frame rendering timer, that prints the time it takes to + render each frame, in microseconds, either on-screen, to stderr, + or both. Valid values are *none*, *osd*, *log* and + *both*. Default: _none_. + +*box-drawing-base-thickness* + Line thickness to use for *LIGHT* box drawing line characters, in + points. This value is converted to pixels using the monitor's DPI, + and then multiplied with the cell size. The end result is that a + larger font (and thus larger cells) result in thicker + lines. Default: _0.04_. + +*box-drawing-solid-shades* + Boolean. When enabled, box drawing "shades" (e.g. LIGHT SHADE, + MEDIUM SHADE and DARK SHADE) are rendered as solid blocks using a + darker variant of the current foreground color. + + When disabled, they are instead rendered as checker box pattern, + using the current foreground color as is. + + Default: _yes_. + +*delayed-render-lower*, *delayed-render-upper* + These two values control the timeouts (in nanoseconds) that are + used to mitigate screen flicker caused by clients writing large, + non-atomic screen updates. + + If a client splits up a screen update over multiple *write*(3) + calls, we may end up rendering an intermediate frame, quickly + followed by another frame with the final screen content. For + example, the client may erase part of the screen (or scroll) in + one write, and then write new content in one or more subsequent + writes. Rendering the frame when the screen has been erased, but + not yet filled with new content will be perceived as screen + flicker. + + The *real* solution to this is _Application Synchronized Updates_ + (https://gitlab.freedesktop.org/terminal-wg/specifications/-/merge_requests/2). + + The problem with this is twofold - first, it has not yet been + standardized, and thus there are not many terminal emulators that + implement it (foot *does* implement it), and second, applications + must be patched to use it. + + Until this has happened, foot offers an interim workaround; an + attempt to mitigate the screen flicker *without* affecting either + performance or latency. + + It is based on the fact that the screen is updated at a fixed + interval (typically 60Hz). For us, this means it does not matter + if we render a new frame at the *beginning* of a frame interval, + or at the *end*. Thus, the goal is to introduce a delay between + receiving client data and rendering the resulting state, but + without causing a frame skip. + + While it should be possible to estimate the amount of time left + until the next frame, foot's algorithm is currently not that + advanced, but is based on statistics I guess you could say - the + delay we introduce is so small that the risk of pushing the frame + over to the next frame interval is also very small. + + Now, that was a lot of text. But what is it foot actually does? + + When receiving client data, it schedules a timer, the + *delayed-render-lower*. If we do not receive any more client data + before the timer has run out, we render the frame. If however, we + do receive more data, the timer is re-scheduled. That is, each + time we receive client data, frame rendering is delayed another + *delayed-render-lower* nanoseconds. + + Now, while this works very well with most clients, it would be + possible to construct a malicious client that keeps writing data + at a slow pace. To the user, this would look like foot has frozen + as we never get to render a new frame. To prevent this, an upper + limit is set - *delayed-render-upper*. If this timer runs out, we + render the frame regardless of what the client is doing. + + If changing these values, note that the lower timeout *must* be + set lower than the upper timeout, but that this is not verified by + foot. Furthermore, both values must be less than 16ms (that is, + 16000000 nanoseconds). + + You can disable the feature altogether by setting either value to + 0. In this case, frames are rendered "as soon as possible". + + Default: lower=_500000_ (0.5ms), upper=_8333333_ (8.3ms - half a + frame interval). + +*damage-whole-window* + Boolean. When enabled, foot will 'damage' the entire window each + time a frame has been rendered. This forces the compositor to + redraw the entire window. If disabled, foot will only 'damage' + updated rows. + + There is normally *no* reason to enable this. However, it has been + seen to workaround an issue with _fractional scaling_ in _Gnome_. + + Note that enabling this option is likely to increase CPU and/or + GPU usage (by the compositor, not by foot), and may have a + negative impact on battery life. + + Default: _no_. + +*grapheme-shaping* + Boolean. When enabled, foot will use _utf8proc_ to do grapheme + cluster segmentation while parsing "printed" text. Then, when + rendering, it will use _fcft_ (if compiled with _HarfBuzz_ + support) to shape the grapheme clusters. + + This is required to render e.g. flag (emoji) sequences, keycap + sequences, modifier sequences, zero-width-joiner (ZWJ) sequences + and emoji tag sequences. It might also improve rendering of + composed characters, depending on font. + + - foot must have been compiled with utf8proc support + - fcft must have been compiled with HarfBuzz support + + This option can also be set runtime with DECSET/DECRST 2027. + + See also: *grapheme-width-method*. + + Default: _yes_ + +*grapheme-width-method* + Selects which method to use when calculating the width + (i.e. number of columns) of a grapheme cluster. One of + *wcswidth*, *double-width* and *max*. + + *wcswidth* simply adds together the individual width of all + codepoints making up the cluster. + + *double-width* does the same, but limits the maximum number of + columns to 2. This is more correct, but may break some + applications since applications typically use *wcswidth*(3) + internally to calculate the width. This results in cursor + de-synchronization issues. + + *max* uses the width of the largest codepoint in the cluster. + + Default: _double-width_ + +*font-monospace-warn* + Boolean. When enabled, foot will use heuristics to try to verify + the primary font is a monospace font, and warn if it is not. + + Disable this if you still want to use the font, even if foot + thinks it is not monospaced. + + You may also want to disable it to get slightly faster startup times. + + Default: _yes_ + +*max-shm-pool-size-mb* + This option controls the amount of virtual address space used by + the pixmap memory to which the terminal screen content is + rendered. + + It does not change how much physical memory foot uses. + + Foot uses a memory mapping trick to implement fast rendering of + interactive scrolling (typically, but applies to "slow" scrolling + in general). Example: holding down the 'up' or 'down' arrow key to + scroll in a text editor. + + For this to work, it needs a large amount of virtual address + space. Again, note that this is not physical memory. + + On a normal x64 based computer, each process has 128TB of virtual + address space, and newer ones have 64PB. This is an insane amount + and most applications do not use anywhere near that amount. + + Each foot terminal window can allocate up to 2GB of virtual + address space. With 128TB of address space, that means a maximum + of 65536 windows in server/daemon mode (for 2GB). That should be + enough, yes? + + However, the Wayland compositor also needs to allocate the same + amount of virtual address space. Thus, it has a slightly higher + chance of running out of address space since it needs to host + all running Wayland clients in the same way, at the same time. + + In the off chance that this becomes a problem for you, you can + reduce the amount used with this option. + + Or, for optimal performance, you can increase it to the maximum + allowed value, 2GB (but note that you most likely will not notice + any difference compared to the default value). + + Setting it to 0 disables the feature. + + Limitations: + - only supported on 64-bit architectures + - only supported on Linux + + Default: _512_. Maximum allowed: _2048_ (2GB). + +*min-stride-alignment* + This option controls the minimum stride alignment, in bytes, when + allocating SHM buffers. + + In some circumstances, a compositor can import foot's SHM buffers + directly to the GPU, without copying the buffer to GPU memory + (typically on integrated graphics). Different drivers have + different requirements for this, and one of those requirements is + typically the stride alignment. At the time of writing, AMD GPUs + require 256-byte alignment. + + Note that doing a direct import typically disables immediate + buffer release (if the compositor supports that), which means foot + has to double buffer. This adds a performance penalty in foot, but + the overall system performance should still be better. + + If you are not using integrated graphics, or if the compositor + does not support GPU direct imports, this option has close to zero + impact. You can save a small amount of memory by setting this to + 0. + + Ultimately, it is up to the compositor to decide whether to do + immediate buffer releases, or try to optimize GPU imports. + + Default: _256_ + +*sixel* + Boolean. When enabled, foot will process sixel images. Default: + _yes_ + +*dim-amount* + Amount by which dimmed text is darkened. Default: _1.5_. + +*bold-text-in-bright-amount* + Amount by which bold fonts are brightened when + *bold-text-in-bright* is set to *yes* (the *palette-based* variant + is not affected by this option). Default: _1.3_. + +*surface-bit-depth* + Selects which RGB bit depth to use for image buffers. One of + *auto*, *8-bit*, *10-bit* or *16-bit*. + + *auto* chooses bit depth depending on other settings, and + availability. + + *8-bit*, uses 8 bits for each color channel, alpha included. This + is the default when *gamma-correct-blending=no*. + + *10-bit* uses 10 bits for each RGB channel, and 2 bits for the + alpha channel. Thus, it provides higher precision color channels, + but a lower precision alpha channel. + + *16-bit* 16 bits for each color channel, alpha included. If + available, this is the default when *gamma-correct-blending=yes*. + + Note that both *10-bit* and *16-bit* are much slower than *8-bit*; + if you want to use gamma-correct blending, and if you prefer speed + (throughput and input latency) over accurate colors, you can set + *surface-bit-depth=8-bit* explicitly. + + Default: _auto_ + +*pre-apply-damage* + Boolean. When enabled, foot will attempt to "pre-apply" the damage + from the last frame when foot is forced to double-buffer + (i.e. when the compositor does not release SHM buffers + immediately). All text after this assumes the compositor is not + releasing buffers immediately. + + When this option is disabled, each time foot needs to render a + frame, it has to first copy over areas that changed in the last + frame (i.e. all changes between the last two frames). This is + basically a *memcpy*(3), which can be slow if the changed area is + large. It is also done on the main thread, which means foot cannot + do anything else at the same time; no other rendering, no VT + parsing. After the changes have been brought over to the new + frame, foot proceeds with rendering the cells that has changed + between the last frame and the new frame. + + When this option is enabled, the changes between the last two frames + are brought over to what will become the next frame before foot + starts rendering the next frame. As soon as the compositor + releases the previous buffer (typically right after foot has + pushed a new frame), foot kicks off a thread that copies over the + changes to the newly released buffer. Since this is done in a + thread, foot can continue processing input at the same + time. Later, when it is time to render a new frame, the changes + have already been transferred, and foot can immediately start with + the actual rendering. + + Thus, having this option enabled improves both performance + (copying the last two frames' changes is threaded), and improves + input latency (rendering the next frame no longer has to first bring + over the changes between the last two frames). + + Default: _yes_ + +# SEE ALSO + +*foot*(1), *footclient*(1) diff --git a/doc/footclient.1.scd b/doc/footclient.1.scd new file mode 100644 index 0000000..ad86591 --- /dev/null +++ b/doc/footclient.1.scd @@ -0,0 +1,214 @@ +footclient(1) + +# NAME +footclient - start new terminals in a foot server + +# SYNOPSIS +*footclient* [_OPTIONS_]++ +*footclient* [_OPTIONS_] <_command_> [_COMMAND OPTIONS_] + +All trailing (non-option) arguments are treated as a command, and its +arguments, to execute (instead of the default shell). + +# DESCRIPTION + +*footclient* is used together with *foot*(1) in *--server* +mode. + +Running it without arguments will open a new terminal window (hosted +in the foot server), with your default shell. The exit code will be +that of the terminal. I.e *footclient* does not exit until the +terminal has terminated. + +# OPTIONS + +*-t*,*--term*=_TERM_ + Value to set the environment variable *TERM* to (see *TERMINFO* + and *ENVIRONMENT*). Default: _@default_terminfo@_. + +*-T*,*--title*=_TITLE_ + Initial window title. Default: _foot_. + +*-a*,*--app-id*=_ID_ + Value to set the *app-id* property on the Wayland window + to. Default: _foot_ (normal mode), or _footclient_ (server mode). + +*toplevel-tag*=_TAG_ + Value to set the *toplevel-tag* property on the Wayland window + to. The compositor can use this value for session management, + window rules etc. Default: _not set_ + +*-w*,*--window-size-pixels*=_WIDTHxHEIGHT_ + Set initial window width and height, in pixels. Default: _700x500_. + +*-W*,*--window-size-chars*=_WIDTHxHEIGHT_ + Set initial window width and height, in characters. Default: _not set_. + +*-m*,*--maximized* + Start in maximized mode. If both *--maximized* and *--fullscreen* + are specified, the _last_ one takes precedence. + +*-F*,*--fullscreen* + Start in fullscreen mode. If both *--maximized* and *--fullscreen* + are specified, the _last_ one takes precedence. + +*-L*,*--login-shell* + Start a login shell, by prepending a '-' to argv[0]. + +*-D*,*--working-directory*=_DIR_ + Initial working directory for the client application. Default: + _CWD of footclient_. + +*-s*,*--server-socket*=_PATH_ + Connect to _PATH_ instead of + *$XDG\_RUNTIME\_DIR/foot-$WAYLAND\_DISPLAY.sock*. + +*-H*,*--hold* + Remain open after child process exits. + +*-N*,*--no-wait* + Detach the client process from the running terminal, exiting + immediately. + +*-o*,*--override*=[_SECTION_.]_KEY_=_VALUE_ + Override an option set in the configuration file. If _SECTION_ is not + given, defaults to _main_. + +*-E*,*--client-environment* + The child process in the new terminal instance will use + footclient's environment, instead of the server's. + + Environment variables listed in the *Variables set in the child + process* section will be overwritten by the foot server. For + example, the new terminal will use *TERM* from the configuration, + not footclient's environment. + +*-d*,*--log-level*={*info*,*warning*,*error*,*none*} + Log level, used both for log output on stderr as well as + syslog. Default: _warning_. + +*-l*,*--log-colorize*=[{*never*,*always*,*auto*}] + Enables or disables colorization of log output on stderr. + +*-v*,*--version* + Show the version number and quit + +*-e* + Ignored; for compatibility with *xterm -e*. See *foot*(1) for more + details. + +# EXIT STATUS + +Footclient will exit with code 220 if there is a failure in footclient +itself (for example, the server socket does not exist). + +If *-N*,*--no-wait* is used, footclient exits with code 0 as soon as +the foot server has been instructed to open a new window. + +If not, footclient may also exit with code 230. This indicates a +failure in the foot server. + +In all other cases the exit code is that of the client application +(i.e. the shell). + +# TERMINFO + +Client applications use the terminfo identifier specified by the +environment variable *TERM* (set by foot) to determine terminal +capabilities. + +Foot has two terminfo definitions: *foot* and *foot-direct*, with +*foot* being the default. + +The difference between the two is in the number of colors they +describe; *foot* describes 256 colors and *foot-direct* 16.7 million +colors (24-bit truecolor). + +Note that using the *foot* terminfo does not limit the number of +usable colors to 256; applications can still use 24-bit RGB colors. In +fact, most applications work best with *foot* (including 24-bit +colors)). Using *\*-direct* terminfo entries has been known to crash +some ncurses applications even. + +There are however applications that need a *\*-direct* terminfo entry +for 24-bit support. Emacs is one such example. + +While using either *foot* or *foot-direct* is strongly recommended, it +is possible to use e.g. *xterm-256color* as well. This can be useful +when remoting to a system where foot's terminfo entries cannot easily +be installed. + +Note that terminfo entries can be installed in the user's home +directory. I.e. if you do not have root access, or if there is no +distro package for foot's terminfo entries, you can install foot's +terminfo entries manually, by copying *foot* and *foot-direct* to +*~/.terminfo/f/*. + +# ENVIRONMENT + +## Variables used by footclient + +*XDG\_RUNTIME\_DIR* + Used to construct the default _PATH_ for the *--server-socket* + option, when no explicit argument is given (see above). + +*WAYLAND\_DISPLAY* + Used to construct the default _PATH_ for the *--server-socket* + option, when no explicit argument is given (see above). + +If the socket at default _PATH_ does not exist, *footclient* will +fallback to the less specific path, with the following priority: +*$XDG\_RUNTIME\_DIR/foot-$WAYLAND\_DISPLAY.sock*, +*$XDG\_RUNTIME\_DIR/foot.sock*, */tmp/foot.sock*. + +## Variables set in the child process + +*TERM* + terminfo/termcap identifier. This is used by client applications + to determine which capabilities a terminal supports. The value is + set according to either the *--term* command-line option or the + *term* config option in *foot.ini*(5). + +*COLORTERM* + This variable is set to *truecolor*, to indicate to client + applications that 24-bit RGB colors are supported. + +*PWD* + Current working directory (at the time of launching foot) + +*SHELL* + Set to the launched shell, if the shell is valid (it is listed in + */etc/shells*). + +In addition to the variables listed above, custom environment +variables may be defined in *foot.ini*(5). + +## Variables *unset* in the child process + +*TERM_PROGRAM* +*TERM_PROGRAM_VERSION* + These environment variables are set by certain other terminal + emulators. We unset them, to prevent applications from + misdetecting foot. + +In addition to the variables listed above, custom environment +variables to unset may be defined in *foot.ini*(5). + +# Signals + +The following signals have special meaning in footclient: + +- SIGUSR1: switch to the dark color theme (*[colors-dark]*). +- SIGUSR2: switch to the light color theme (*[colors-light]*). + +When sending SIGUSR1/SIGUSR2 to a footclient instance, the theme is +changed in that instance only. This is different from when you send +SIGUSR1/SIGUSR2 to the server process, where all instances change the +theme. + +Note: for obvious reasons, this is not supported when footclient is +started with *--no-wait*. + +# SEE ALSO + +*foot*(1) diff --git a/doc/meson.build b/doc/meson.build new file mode 100644 index 0000000..3797265 --- /dev/null +++ b/doc/meson.build @@ -0,0 +1,49 @@ +scdoc_prog = find_program(scdoc.get_variable('scdoc'), native: true) + +if utmp_backend != 'none' + utmp_add_args = '@0@ $WAYLAND_DISPLAY'.format(utmp_add) + utmp_del_args = (utmp_del_have_argument + ? '@0@ $WAYLAND_DISPLAY'.format(utmp_del) + : '@0@'.format(utmp_del)) + utmp_path = utmp_default_helper_path +else + utmp_add_args = '<no utmp support in foot>' + utmp_del_args = '<no utmp support in foot>' + utmp_path = 'none' +endif + + +conf_data = configuration_data( + { + 'default_terminfo': get_option('default-terminfo'), + 'utmp_backend': utmp_backend, + 'utmp_add_args': utmp_add_args, + 'utmp_del_args': utmp_del_args, + 'utmp_helper_path': utmp_path, + } +) + +foreach man_src : [{'name': 'foot', 'section' : 1}, + {'name': 'foot.ini', 'section': 5}, + {'name': 'footclient', 'section': 1}, + {'name': 'foot-ctlseqs', 'section': 7}] + name = man_src['name'] + section = man_src['section'] + out = '@0@.@1@'.format(name, section) + + preprocessed = configure_file( + input: '@0@.@1@.scd'.format(name, section), + output: '@0@.preprocessed'.format(out), + configuration: conf_data, + ) + + custom_target( + out, + output: out, + input: preprocessed, + command: scdoc_prog.full_path(), + capture: true, + feed: true, + install: true, + install_dir: join_paths(get_option('mandir'), 'man@0@'.format(section))) +endforeach diff --git a/doc/sixel-tux-foot.png b/doc/sixel-tux-foot.png new file mode 100644 index 0000000000000000000000000000000000000000..ce30fe8fff52aa58817ded0ddf44350cea73d87d GIT binary patch literal 297553 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�__<#$&4>} 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%0DCnlyh9awGq?&TSK7Bgvw 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 F33DAeRbB|^YQ7o_IcM@ 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 ?5g`|;*WBU zK03v#86IoQ8@H`*dG+RPzu$D8H>ilVwU<No)5qiT|4%=#XOmiPuD$*CzT<qwJFhg} z(@a(Vdo=mIL$&8Fk>HHmC-=YaICVLm=WMn6FXt_OVaqG)Dk9(S%+{8*{j>A-y3Oxb z-9GIxe`hq$(ddfUg|)UUd*{gRhsNCgcz6tMe{Ij@l`GbJX}0{|munYp-Z^vH>1}@f z51-qp=>6K<y=~8y)}^QYK2O-a;;pFY{?$6%m-F@7iYH&47pNVs-@0<^>NR$6otJ(J zim=O@l=FREh+--C;q^MVZSPI{{61!d?&{pxGRF<}%Wr27vs0OxuK4f9v@PpH_PFI9 zK#9eV$M?tn54<${{5;#+f9Br%y|1?YtX9S2x5m#(q;Hm=zv;)XeERCT(q*U9EMKX` z$h}`5qFhuu=k50?SC@Z&<x#p$uI1AeU6=V!mqhP<T03w3y5E=MLhtp>cs);k^XpCZ zDsSDFvM)b72ONP&ME>>qD~{_krrncqzU}n$=GD13?Yw`*@qCN?bYA4yyRX})zWn)o z<sJd&8<8CM4!>5DdA+%yuCjM(tlVd-XY1B~>$~LqIk}Q=_j<K7?b%1omY1dK#ii@| z%fh0P{|Eb@kNiI#_t)?K_jtigr?8u8N2V#ydu(}K{SmX~H1^78M?XE7_0aA3{V22B zmb<rE3Y-mCzWTAv`s(H4tN*s<*w(yHslO1H_N&M8%~q>N_H9$GL(Gsf-sArIqQm<` z{|9E8yx1DqIQ3vxd{)C>y=mFT%w;u7(Uxy+Reikop`?C=+aJBhvag=+;aey^KR489 z`uCY#Zx~l@&TUgF+x1r6bn@Pwxo0ghQa61mIP^gN(5?Az3#Yz1gdRbm{|hh7oIn5m zDSn&pzoKvJMOFN3H0jzNP+p(+VfPf7d4V^2|IAuB+rMqA`E<6;LR{tPn;CD}{5<lg zXYP6Va|M=RVK+0cDL(CKI_q_$sH8?W+Vjl$w;|8wz5VtYDNew-JO6C`w{ypxgP-N^ z-T3YImzS5fO$c4=`2JITo!yT>o$FH5TRt62ooiU5_x`}%Pg~m8pO%kWwYY}Cy6y1Q ziNDxyJiW7x{ax<KTXX+SbXoyU>g>qbIR9{c*}v@zH4iu63y-T*EwP%L_VTRUq;1EJ z{CSla#OzwW?Hc2}@3Gtyt1XWz*sQ;}_x|R4wZ>CZWf!pDSi8R_wt9uD?V0t(tNgDo zkNL3Zl==N@b?^TyloZldZ@d5T`2K5oe?A_!e*n*6^4tDvWE{|z%8mT8IloS~<?-ch z(}VucI-4b#)%&bJcJjBxpyuwQC$s!l)Ofx=_h81fjbV}t|Ln7?J$33@^5iM!{MR1a z^J&wae{Skm=TF<JcY038{LG*4`0Z9qd-X2mx6jYoy`j6~e%svqxFe-3E8b>V`^J57 zA#b(&e|`;{_b&bw!Xc2%e}B_|<@zd1lhlgSi^}iUUbp)`_g>E8cMCs!$@_Wd<*mJa zqJ|>xEblr$)vAxtH@rS=|2@@``R7gET)AiSWZ&hz)$_o~8XQef;=kBm_sZqpw_cCi zTwXPM?L8Uk<p$f$E#BUJB5V66HYQ@t+|B21?lFis=*@4?c4+w{uN(V{W~{sV>9*DL zLx-Hdm-(*Wr!=!PjHf-ieQN2syBEBlU-O^in|sAn^NsxZ=okC<obSJW>KGz9+CPy0 zaD0Cm@4xof_FPgQy4+r#{r>;KYO$hi8_JG2y;{T*|9ZEl&m8XHqf=_X7i?=iclyJ( zDl_Q~g@u>SeNPvEyZgh0U5B1p-Mcx<?|Deusi1VtEAO7Rb+1l*cB3tJ@rPaQ$M@!K z%}su}cf+QryUEX!JK{4LR~^=v7Ca+9_e+)TlelXu??3(){<k<>{~<h<A%VNU`@i#f z8JoS8iHQg8r1`6K#jVSFxR<|ootmwe_<l{y9lPq)w?Ca*Yn;41Bh2Bt*NyKrukM>9 z^XDcN-KtxCd2jRA4Z+7(zq_il=N<pX@TW^&?<)S-Xc5=9b?cwy5t`|uzob7lx`t2P zyfN;qQP1u8<LDXZ{@wo*?e|H{+H~7`Z||kqp^2x2-=AOlcw5W1sY2m@cl}Ix#lzn$ zeN@3ex~MWDHnrl!y>Q;m?YsVMd;56G{q)i)*CS@w@i=`kd;Kt1cYk%qo%O=|J{HO~ zeK$LIw`}fP&eWF-#`*chNO1v<mHoB<18;;^eb(P!{#xNr>^Xyo8GCu$*MDDi^>9t8 z9{<wS$u_gHk4)HgK`W6f_BV%p{okK|*M`p4-5`2WZF%-)u74}5s^!mxZDw7yd5-Rj z^J|v{>^Sv(!u~Y-OF3<~R$bWH{yO>Qub90SQ`2m8p4?w~=zXT^=A3EFuyPib49otw zcYfLT`JDCdh!d-%&Dr}bZ*FT3wiNm<xU2Wo<BWUt%Wt+PZ8$#pMEFmy&5R$V=J#IU zK2zp@vy$udZ&NqRN7r*&&i&r!x39D4-myJ=^^Df@YbEm9Uv1pGcGI?1vcErW4?Xb# zY8T9E`TqYB8Cy~>9{S9GBkiE3i`+(oFV9-PtUPDCds`&`((>2Z`Ag58K6h0v%;sGx z-}SA%3#~+w;;SE@eRbo4<!cY!?`t>w3D{khlUyc`TCm&yd;6<@Q|iT;#_4Z12QCSp z`gWFZ9M80k*LV&b**WuIZ|Y;+=P`3--+LDYp6RR1t2nvy@$AerCBgS!V+!U!-0*v| z-_<n^^Vr|*d}|l|z*sml^39EPCfW~=|Ead=LvFk=*8lmqU*{kD(%Wy_;?`e(GxuKg zyPru@<=5G~c^TY%b+6Z!DW&_qP6&HB>(`N^Q_R=>$vyr4T=>69F6l)RzRQMx4bm&$ zZ$5e2dcV^J$FtX-`Lyfbr`Cgit1?ohb>eFmPd;^R`T0<hxv$x@yQb#O?~q;Tzx8VI znXBJwGIP%>e=ox*o96wu2+Pkic~Se~-0js}r`c8)e`VTSzUa`?%>B!6aIWs2ddFqk zl5M4*QcCCQPPE-D;}^G8(CnJtE!$Iq-#_1zJbHBJm4(_H{Z@a!VfvMCca`Rs@NGeV zf11vc-SBtmBWJ(w$NKzouTHx4^iA{rDY^fBQmwyQ|DBAUiSN()zk)kb)b(P{h2P!s zb=sltWY<sFnI;x<ChSYpt1I97KeavMKP=~T(=%%6>69(Eu77;_s`1OOt&uG?%&GgY zo11NG7m=U$K5*&ptCc@>PW*h-mep!_HsFW-S+A`{Gn`&z{$Fv!?rQ#h=MAA3-^ja_ z9@&MSjq{KHUzi@p=X*2v>$9|IvD*LNvN~TCO?ltDskg&+N?ekx(DVb}`mQ+Kbh;`R zbiFkD>rAfMf*tp3SHAkfu-kiO&BMg}*CDdsCseM<KlGpT5%Z?xRn<jSS0B#J(~EAF z<i9U-^U&Yy(7)dfY{s<u{QvO!AK4kF{9c}&>$m*jG*{Iqwdl<pTc<C0%$4}-oxbGj zr75wCle^RV!}V3BU;lafx|q}Vvr|v=T`CO=i|o^%x%P40{0-9={*Bmke&e6d3q?<T zaVov%mOuMdz-*q}vX~=g*+1XrSmtfLn&$J!7O5KnE<xkv{x>Hv$LXeBRk>9CpMRg# z49kwh&z&Jbe^p;k4f6Z>)J!?B??-Ulk5dWhOCJ55P_(aB%;Eaa751&iA9tUZdicBT zw9@^a$i%Yxz}sntVXyRye)qIaUCP_P{{MwbjB9$nX0dMEl32W+YrX&4*r{m}doX%4 z+h5zCvfj(h%{}d2&6<;Ef8UhmWB`K)UJEZS5C=0E9xQGPnj`sz3Cue%(V02!AWW)H zho_1WEdJocA-JGQZ+Jc%Sd`(1#zMyR2^Y8_Ja<0%FJ;US_5s0W4}AzzCe!4D>Ls`u zrM(d0f|QpfR^Lv-jg?u;Q{{iJ$lapy^K_@}mw8<`N`{I3mllirAI)*p4Wfs^B8RX1 zLiJ<uoCDgs?#9j7`^`A<)2i%4JOA&M+UAsa{Cc>43;+53vQX8>Iv4G2w|epRo7y(n z*>mg!*ZqETJ^T2*-`~3*{)t}SwyfsgKKa|v_sji`{q^=f#G?#)ubsl?8om0x^89^4 zzj?bfzw~7${JHz#*Zu!8|9&1?uV1;IzoLCV)R#{(9^B5j@cYBv^44WGKdxN9-S_qR zI{xF!)8AAtzs$Sbt~PqcteI<Li%(DAe=*~N_9u?~f6@@Q9muqKah83P6vKOy4R?F} z>)w4Ucs1>O+^1dq)2BNLe`3^Yj)f?^!O2@8&0ppI@YU=5tdhI8etw6<6vF`v%PWrS z@7=XEv%LD^7XP`uP?H&s9bLHop3P<ZiDoSOL{4|-vqB7NC@fmheD87a*{*o2-)=w7 zpVntVmZ-P+z2Yx447PN>zkHh)BFo^;BmYJ2*Q>Ma4?BOqyZQCb-hNw2sMM?rw-$WA zd4G?{zR#1_o9wGwe(-Hvb0So7`BsnhY`cGc|I1SYF?NCE`TsALepq|IKK1_dSyQyz z-rx718F$QJSN;0p#}5M7_u4(U>TsQX4@~{B(|OFtz2{nI>}1z1Uf=j;2is1|_I>{E z{R|#Jyd)vNn5Uw=`_a47_Zq3&Jhw-$y?y7@tFx6|#t<j;v};=~{aLiw@qMGxUVgi; zB^5v3?SAt)sKRQqa~4F+fgcj4u@&n%-upf0i+WaY(>iwh^_|TN7}r|_RBh(3DYu?} z_y8oIJa~N8k2$7a>V0Wmh+Eb2?Vd}nOU9kxg4m>I|GRSe_LmM{yegN6M~l5&zxC>D zh^HT%O5eWaaUk19U$*Oy<y3wz*luiZzklldig#bPAF`0_dt)ZaU|)FO_Tjzb^Kxor zz{Wjz>>>U8{<G#wfv05lneR_uU+g<`|JP%;-W#W%lc`%0{zGAUc_qa7W8Tug?>&3k z6ZGuqjm6uq)>cV=fFuux=q;c8pc(MM(+6GhUtax%rl^MI0$2X3$Td*6yDik<`uzr) zp&OLj#5M};xeUv#lfvM6J)=8syH`#cJhKbAR{iIHCo+#U>CV$~kh?uy{an^LB{Ts5 D_T*7t literal 0 HcmV?d00001 diff --git a/doc/tux-foot-ok.png b/doc/tux-foot-ok.png new file mode 100644 index 0000000000000000000000000000000000000000..5d814623eb7d968a03620ef279bfa8d1cda07903 GIT binary patch literal 404008 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�G?$4t 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%0AF6fRy8SQD&noFSPI;3D6L0Wu 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 literal 0 HcmV?d00001 diff --git a/extract.c b/extract.c new file mode 100644 index 0000000..cd9a0c9 --- /dev/null +++ b/extract.c @@ -0,0 +1,271 @@ +#include "extract.h" +#include <string.h> + +#define LOG_MODULE "extract" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "char32.h" + +struct extraction_context { + char32_t *buf; + size_t size; + size_t idx; + size_t tab_spaces_left; + size_t empty_count; + size_t newline_count; + bool strip_trailing_empty; + bool failed; + const struct row *last_row; + const struct cell *last_cell; + enum selection_kind selection_kind; +}; + +struct extraction_context * +extract_begin(enum selection_kind kind, bool strip_trailing_empty) +{ + struct extraction_context *ctx = malloc(sizeof(*ctx)); + if (unlikely(ctx == NULL)) { + LOG_ERRNO("malloc() failed"); + return NULL; + } + + *ctx = (struct extraction_context){ + .selection_kind = kind, + .strip_trailing_empty = strip_trailing_empty, + }; + return ctx; +} + +static bool +ensure_size(struct extraction_context *ctx, size_t additional_chars) +{ + while (ctx->size < ctx->idx + additional_chars) { + size_t new_size = ctx->size == 0 ? 512 : ctx->size * 2; + char32_t *new_buf = realloc(ctx->buf, new_size * sizeof(new_buf[0])); + + if (new_buf == NULL) + return false; + + ctx->buf = new_buf; + ctx->size = new_size; + } + + xassert(ctx->size >= ctx->idx + additional_chars); + return true; +} + +bool +extract_finish_wide(struct extraction_context *ctx, char32_t **text, size_t *len) +{ + if (text == NULL) + return false; + + *text = NULL; + if (len != NULL) + *len = 0; + + if (ctx->failed) + goto err; + + if (!ctx->strip_trailing_empty) { + /* Insert pending newlines, and replace empty cells with spaces */ + if (!ensure_size(ctx, ctx->newline_count + ctx->empty_count)) + goto err; + + for (size_t i = 0; i < ctx->newline_count; i++) + ctx->buf[ctx->idx++] = U'\n'; + + for (size_t i = 0; i < ctx->empty_count; i++) + ctx->buf[ctx->idx++] = U' '; + } + + if (ctx->idx == 0) { + /* Selection of empty cells only */ + if (!ensure_size(ctx, 1)) + goto err; + ctx->buf[ctx->idx++] = U'\0'; + } else { + xassert(ctx->idx > 0); + xassert(ctx->idx <= ctx->size); + + switch (ctx->selection_kind) { + default: + if (ctx->buf[ctx->idx - 1] == U'\n') + ctx->buf[ctx->idx - 1] = U'\0'; + break; + + case SELECTION_LINE_WISE: + if (ctx->buf[ctx->idx - 1] != U'\n') { + if (!ensure_size(ctx, 1)) + goto err; + ctx->buf[ctx->idx++] = U'\n'; + } + break; + + } + + if (ctx->buf[ctx->idx - 1] != U'\0') { + if (!ensure_size(ctx, 1)) + goto err; + ctx->buf[ctx->idx++] = U'\0'; + } + } + + *text = ctx->buf; + if (len != NULL) + *len = ctx->idx - 1; + free(ctx); + return true; + +err: + free(ctx->buf); + free(ctx); + return false; +} + +bool +extract_finish(struct extraction_context *ctx, char **text, size_t *len) +{ + if (text == NULL) + return false; + if (len != NULL) + *len = 0; + + char32_t *wtext; + if (!extract_finish_wide(ctx, &wtext, NULL)) + return false; + + bool ret = false; + + *text = ac32tombs(wtext); + if (*text == NULL) { + LOG_ERR("failed to convert selection to UTF-8"); + goto out; + } + + if (len != NULL) + *len = strlen(*text); + ret = true; + +out: + free(wtext); + return ret; +} + +bool +extract_one(const struct terminal *term, const struct row *row, + const struct cell *cell, int col, void *context) +{ + struct extraction_context *ctx = context; + + if (cell->wc >= CELL_SPACER) + return true; + + if (ctx->last_row != NULL && row != ctx->last_row) { + /* New row - determine if we should insert a newline or not */ + + if (ctx->selection_kind != SELECTION_BLOCK) { + if (ctx->last_row->linebreak || + ctx->empty_count > 0 || + cell->wc == 0) + { + /* Row has a hard linebreak, or either last cell or + * current cell is empty */ + + /* Don't emit newline just yet - only if there are + * non-empty cells following it */ + ctx->newline_count++; + + if (!ctx->strip_trailing_empty) { + if (!ensure_size(ctx, ctx->empty_count)) + goto err; + for (size_t i = 0; i < ctx->empty_count; i++) + ctx->buf[ctx->idx++] = U' '; + } + ctx->empty_count = 0; + } + } else { + /* Always insert a linebreak */ + if (!ensure_size(ctx, 1)) + goto err; + + ctx->buf[ctx->idx++] = U'\n'; + + if (!ctx->strip_trailing_empty) { + if (!ensure_size(ctx, ctx->empty_count)) + goto err; + for (size_t i = 0; i < ctx->empty_count; i++) + ctx->buf[ctx->idx++] = U' '; + } + ctx->empty_count = 0; + } + + ctx->tab_spaces_left = 0; + } + + if (cell->wc == U' ' && ctx->tab_spaces_left > 0) { + ctx->tab_spaces_left--; + return true; + } + + ctx->tab_spaces_left = 0; + + if (cell->wc == 0) { + ctx->empty_count++; + ctx->last_row = row; + ctx->last_cell = cell; + return true; + } + + /* Insert pending newlines, and replace empty cells with spaces */ + if (!ensure_size(ctx, ctx->newline_count + ctx->empty_count)) + goto err; + + for (size_t i = 0; i < ctx->newline_count; i++) + ctx->buf[ctx->idx++] = U'\n'; + + for (size_t i = 0; i < ctx->empty_count; i++) + ctx->buf[ctx->idx++] = U' '; + + ctx->newline_count = 0; + ctx->empty_count = 0; + + if (cell->wc >= CELL_COMB_CHARS_LO && cell->wc <= CELL_COMB_CHARS_HI) + { + const struct composed *composed = composed_lookup( + term->composed, cell->wc - CELL_COMB_CHARS_LO); + + if (!ensure_size(ctx, composed->count)) + goto err; + + for (size_t i = 0; i < composed->count; i++) + ctx->buf[ctx->idx++] = composed->chars[i]; + } + + else { + if (!ensure_size(ctx, 1)) + goto err; + ctx->buf[ctx->idx++] = cell->wc; + + if (cell->wc == U'\t') { + int next_tab_stop = term->cols - 1; + tll_foreach(term->tab_stops, it) { + if (it->item > col) { + next_tab_stop = it->item; + break; + } + } + + if (next_tab_stop > col) + ctx->tab_spaces_left = next_tab_stop - col - 1; + } + } + + ctx->last_row = row; + ctx->last_cell = cell; + return true; + +err: + ctx->failed = true; + return false; +} diff --git a/extract.h b/extract.h new file mode 100644 index 0000000..30bec49 --- /dev/null +++ b/extract.h @@ -0,0 +1,21 @@ +#pragma once + +#include <stddef.h> +#include <stdbool.h> +#include <uchar.h> + +#include "terminal.h" + +struct extraction_context; + +struct extraction_context *extract_begin( + enum selection_kind kind, bool strip_trailing_empty); + +bool extract_one( + const struct terminal *term, const struct row *row, const struct cell *cell, + int col, void *context); + +bool extract_finish( + struct extraction_context *context, char **text, size_t *len); +bool extract_finish_wide( + struct extraction_context *context, char32_t **text, size_t *len); diff --git a/fdm.c b/fdm.c new file mode 100644 index 0000000..4822cd9 --- /dev/null +++ b/fdm.c @@ -0,0 +1,496 @@ +#include "fdm.h" + +#include <stdlib.h> +#include <stdbool.h> +#include <inttypes.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <signal.h> + +#include <sys/epoll.h> + +#include <tllist.h> + +#define LOG_MODULE "fdm" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" +#include "xmalloc.h" + +#if !defined(SIGABBREV_NP) +#include <stdio.h> + +static const char * +sigabbrev_np(int sig) +{ + static char buf[16]; + snprintf(buf, sizeof(buf), "<%d>", sig); + return buf; +} +#endif + +struct fd_handler { + int fd; + int events; + fdm_fd_handler_t callback; + void *callback_data; + bool deleted; +}; + +struct sig_handler { + fdm_signal_handler_t callback; + void *callback_data; +}; + +struct hook { + fdm_hook_t callback; + void *callback_data; +}; + +typedef tll(struct hook) hooks_t; + +struct fdm { + int epoll_fd; + bool is_polling; + tll(struct fd_handler *) fds; + tll(struct fd_handler *) deferred_delete; + + sigset_t sigmask; + struct sig_handler *signal_handlers; + + hooks_t hooks_low; + hooks_t hooks_normal; + hooks_t hooks_high; +}; + +static volatile sig_atomic_t got_signal = false; +static volatile sig_atomic_t *received_signals = NULL; + +struct fdm * +fdm_init(void) +{ + sigset_t sigmask; + if (sigprocmask(0, NULL, &sigmask) < 0) { + LOG_ERRNO("failed to get process signal mask"); + return NULL; + } + + int epoll_fd = epoll_create1(EPOLL_CLOEXEC); + if (epoll_fd == -1) { + LOG_ERRNO("failed to create epoll FD"); + return NULL; + } + + xassert(received_signals == NULL); /* Only one FDM instance supported */ + received_signals = xcalloc(SIGRTMAX, sizeof(received_signals[0])); + got_signal = false; + + struct fdm *fdm = malloc(sizeof(*fdm)); + if (unlikely(fdm == NULL)) { + LOG_ERRNO("malloc() failed"); + return NULL; + } + + struct sig_handler *sig_handlers = calloc(SIGRTMAX, sizeof(sig_handlers[0])); + + if (sig_handlers == NULL) { + LOG_ERRNO("failed to allocate signal handler array"); + free(fdm); + return NULL; + } + + *fdm = (struct fdm){ + .epoll_fd = epoll_fd, + .is_polling = false, + .fds = tll_init(), + .deferred_delete = tll_init(), + .sigmask = sigmask, + .signal_handlers = sig_handlers, + .hooks_low = tll_init(), + .hooks_normal = tll_init(), + .hooks_high = tll_init(), + }; + return fdm; +} + +void +fdm_destroy(struct fdm *fdm) +{ + if (fdm == NULL) + return; + + if (tll_length(fdm->fds) > 0) + LOG_WARN("FD list not empty"); + + for (int i = 0; i < SIGRTMAX; i++) { + if (fdm->signal_handlers[i].callback != NULL) + LOG_WARN("handler for signal %d (SIG%s) not removed", + i, sigabbrev_np(i)); + } + + if (tll_length(fdm->hooks_low) > 0 || + tll_length(fdm->hooks_normal) > 0 || + tll_length(fdm->hooks_high) > 0) + { + LOG_WARN("hook list not empty"); + } + + xassert(tll_length(fdm->fds) == 0); + xassert(tll_length(fdm->deferred_delete) == 0); + xassert(tll_length(fdm->hooks_low) == 0); + xassert(tll_length(fdm->hooks_normal) == 0); + xassert(tll_length(fdm->hooks_high) == 0); + + sigprocmask(SIG_SETMASK, &fdm->sigmask, NULL); + free(fdm->signal_handlers); + + tll_free(fdm->fds); + tll_free(fdm->deferred_delete); + tll_free(fdm->hooks_low); + tll_free(fdm->hooks_normal); + tll_free(fdm->hooks_high); + close(fdm->epoll_fd); + free(fdm); + + free((void *)received_signals); + received_signals = NULL; +} + +bool +fdm_add(struct fdm *fdm, int fd, int events, fdm_fd_handler_t cb, void *data) +{ +#if defined(_DEBUG) + tll_foreach(fdm->fds, it) { + if (it->item->fd == fd) { + BUG("FD=%d already registered", fd); + } + } +#endif + + struct fd_handler *handler = malloc(sizeof(*handler)); + if (unlikely(handler == NULL)) { + LOG_ERRNO("malloc() failed"); + return false; + } + + *handler = (struct fd_handler) { + .fd = fd, + .events = events, + .callback = cb, + .callback_data = data, + .deleted = false, + }; + + tll_push_back(fdm->fds, handler); + + struct epoll_event ev = { + .events = events, + .data = {.ptr = handler}, + }; + + if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_ADD, fd, &ev) < 0) { + LOG_ERRNO("failed to register FD=%d with epoll", fd); + free(handler); + tll_pop_back(fdm->fds); + return false; + } + + return true; +} + +static bool +fdm_del_internal(struct fdm *fdm, int fd, bool close_fd) +{ + if (fd == -1) + return true; + + tll_foreach(fdm->fds, it) { + if (it->item->fd != fd) + continue; + + if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_DEL, fd, NULL) < 0) + LOG_ERRNO("failed to unregister FD=%d from epoll", fd); + + if (close_fd) + close(it->item->fd); + + it->item->deleted = true; + if (fdm->is_polling) + tll_push_back(fdm->deferred_delete, it->item); + else + free(it->item); + + tll_remove(fdm->fds, it); + return true; + } + + LOG_ERR("no such FD: %d", fd); + close(fd); + return false; +} + +bool +fdm_del(struct fdm *fdm, int fd) +{ + return fdm_del_internal(fdm, fd, true); +} + +bool +fdm_del_no_close(struct fdm *fdm, int fd) +{ + return fdm_del_internal(fdm, fd, false); +} + +static bool +event_modify(struct fdm *fdm, struct fd_handler *fd, int new_events) +{ + if (new_events == fd->events) + return true; + + struct epoll_event ev = { + .events = new_events, + .data = {.ptr = fd}, + }; + + if (epoll_ctl(fdm->epoll_fd, EPOLL_CTL_MOD, fd->fd, &ev) < 0) { + LOG_ERRNO("failed to modify FD=%d with epoll (events 0x%08x -> 0x%08x)", + fd->fd, fd->events, new_events); + return false; + } + + fd->events = new_events; + return true; +} + +bool +fdm_event_add(struct fdm *fdm, int fd, int events) +{ + tll_foreach(fdm->fds, it) { + if (it->item->fd != fd) + continue; + + return event_modify(fdm, it->item, it->item->events | events); + } + + LOG_ERR("FD=%d not registered with the FDM", fd); + return false; +} + +bool +fdm_event_del(struct fdm *fdm, int fd, int events) +{ + tll_foreach(fdm->fds, it) { + if (it->item->fd != fd) + continue; + + return event_modify(fdm, it->item, it->item->events & ~events); + } + + LOG_ERR("FD=%d not registered with the FDM", fd); + return false; +} + +static hooks_t * +hook_priority_to_list(struct fdm *fdm, enum fdm_hook_priority priority) +{ + switch (priority) { + case FDM_HOOK_PRIORITY_LOW: return &fdm->hooks_low; + case FDM_HOOK_PRIORITY_NORMAL: return &fdm->hooks_normal; + case FDM_HOOK_PRIORITY_HIGH: return &fdm->hooks_high; + } + + BUG("unhandled priority type"); + return NULL; +} + +bool +fdm_hook_add(struct fdm *fdm, fdm_hook_t hook, void *data, + enum fdm_hook_priority priority) +{ + hooks_t *hooks = hook_priority_to_list(fdm, priority); + +#if defined(_DEBUG) + tll_foreach(*hooks, it) { + if (it->item.callback == hook) { + LOG_ERR("hook=0x%" PRIxPTR " already registered", (uintptr_t)hook); + return false; + } + } +#endif + + tll_push_back(*hooks, ((struct hook){hook, data})); + return true; +} + +bool +fdm_hook_del(struct fdm *fdm, fdm_hook_t hook, enum fdm_hook_priority priority) +{ + hooks_t *hooks = hook_priority_to_list(fdm, priority); + + tll_foreach(*hooks, it) { + if (it->item.callback != hook) + continue; + + tll_remove(*hooks, it); + return true; + } + + LOG_WARN("hook=0x%" PRIxPTR " not registered", (uintptr_t)hook); + return false; +} + +static void +signal_handler(int signo) +{ + got_signal = true; + received_signals[signo] = true; +} + +bool +fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *data) +{ + if (fdm->signal_handlers[signo].callback != NULL) { + LOG_ERR("signal %d (SIG%s) already has a handler", + signo, sigabbrev_np(signo)); + return false; + } + + sigset_t mask, original; + sigemptyset(&mask); + sigaddset(&mask, signo); + + if (sigprocmask(SIG_BLOCK, &mask, &original) < 0) { + LOG_ERRNO("failed to block signal %d (SIG%s)", + signo, sigabbrev_np(signo)); + return false; + } + + struct sigaction action = {.sa_handler = &signal_handler}; + sigemptyset(&action.sa_mask); + if (sigaction(signo, &action, NULL) < 0) { + LOG_ERRNO("failed to set signal handler for signal %d (SIG%s)", + signo, sigabbrev_np(signo)); + sigprocmask(SIG_SETMASK, &original, NULL); + return false; + } + + received_signals[signo] = false; + fdm->signal_handlers[signo].callback = handler; + fdm->signal_handlers[signo].callback_data = data; + return true; +} + +bool +fdm_signal_del(struct fdm *fdm, int signo) +{ + if (fdm->signal_handlers[signo].callback == NULL) + return false; + + struct sigaction action = {.sa_handler = SIG_DFL}; + sigemptyset(&action.sa_mask); + if (sigaction(signo, &action, NULL) < 0) { + LOG_ERRNO("failed to restore signal handler for signal %d (SIG%s)", + signo, sigabbrev_np(signo)); + return false; + } + + received_signals[signo] = false; + fdm->signal_handlers[signo].callback = NULL; + fdm->signal_handlers[signo].callback_data = NULL; + + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, signo); + if (sigprocmask(SIG_UNBLOCK, &mask, NULL) < 0) { + LOG_ERRNO("failed to unblock signal %d (SIG%s)", + signo, sigabbrev_np(signo)); + return false; + } + + return true; +} + +bool +fdm_poll(struct fdm *fdm) +{ + xassert(!fdm->is_polling && "nested calls to fdm_poll() not allowed"); + if (fdm->is_polling) { + LOG_ERR("nested calls to fdm_poll() not allowed"); + return false; + } + + tll_foreach(fdm->hooks_high, it) { + LOG_DBG( + "executing high priority hook 0x%" PRIxPTR" (fdm=%p, data=%p)", + (uintptr_t)it->item.callback, (void *)fdm, + (void *)it->item.callback_data); + it->item.callback(fdm, it->item.callback_data); + } + tll_foreach(fdm->hooks_normal, it) { + LOG_DBG( + "executing normal priority hook 0x%" PRIxPTR " (fdm=%p, data=%p)", + (uintptr_t)it->item.callback, (void *)fdm, + (void *)it->item.callback_data); + it->item.callback(fdm, it->item.callback_data); + } + tll_foreach(fdm->hooks_low, it) { + LOG_DBG( + "executing low priority hook 0x%" PRIxPTR " (fdm=%p, data=%p)", + (uintptr_t)it->item.callback, (void *)fdm, + (void *)it->item.callback_data); + it->item.callback(fdm, it->item.callback_data); + } + + struct epoll_event events[tll_length(fdm->fds)]; + + int r = epoll_pwait( + fdm->epoll_fd, events, tll_length(fdm->fds), -1, &fdm->sigmask); + + int errno_copy = errno; + + if (unlikely(got_signal)) { + got_signal = false; + + for (int i = 0; i < SIGRTMAX; i++) { + if (received_signals[i]) { + received_signals[i] = false; + struct sig_handler *handler = &fdm->signal_handlers[i]; + + xassert(handler->callback != NULL); + if (!handler->callback(fdm, i, handler->callback_data)) + return false; + } + } + } + + if (unlikely(r < 0)) { + if (errno_copy == EINTR) + return true; + + LOG_ERRNO_P(errno_copy, "failed to epoll"); + return false; + } + + bool ret = true; + + fdm->is_polling = true; + for (int i = 0; i < r; i++) { + struct fd_handler *fd = events[i].data.ptr; + if (fd->deleted) + continue; + + if (!fd->callback(fdm, fd->fd, events[i].events, fd->callback_data)) { + ret = false; + break; + } + } + fdm->is_polling = false; + + tll_foreach(fdm->deferred_delete, it) { + free(it->item); + tll_remove(fdm->deferred_delete, it); + } + + return ret; +} diff --git a/fdm.h b/fdm.h new file mode 100644 index 0000000..2ccff95 --- /dev/null +++ b/fdm.h @@ -0,0 +1,34 @@ +#pragma once + +#include <stdbool.h> + +struct fdm; + +typedef bool (*fdm_fd_handler_t)(struct fdm *fdm, int fd, int events, void *data); +typedef bool (*fdm_signal_handler_t)(struct fdm *fdm, int signo, void *data); +typedef void (*fdm_hook_t)(struct fdm *fdm, void *data); + +enum fdm_hook_priority { + FDM_HOOK_PRIORITY_LOW, + FDM_HOOK_PRIORITY_NORMAL, + FDM_HOOK_PRIORITY_HIGH +}; + +struct fdm *fdm_init(void); +void fdm_destroy(struct fdm *fdm); + +bool fdm_add(struct fdm *fdm, int fd, int events, fdm_fd_handler_t handler, void *data); +bool fdm_del(struct fdm *fdm, int fd); +bool fdm_del_no_close(struct fdm *fdm, int fd); + +bool fdm_event_add(struct fdm *fdm, int fd, int events); +bool fdm_event_del(struct fdm *fdm, int fd, int events); + +bool fdm_hook_add(struct fdm *fdm, fdm_hook_t hook, void *data, + enum fdm_hook_priority priority); +bool fdm_hook_del(struct fdm *fdm, fdm_hook_t hook, enum fdm_hook_priority priority); + +bool fdm_signal_add(struct fdm *fdm, int signo, fdm_signal_handler_t handler, void *data); +bool fdm_signal_del(struct fdm *fdm, int signo); + +bool fdm_poll(struct fdm *fdm); diff --git a/foot-features.c b/foot-features.c new file mode 100644 index 0000000..8e33251 --- /dev/null +++ b/foot-features.c @@ -0,0 +1,42 @@ +#include "foot-features.h" +#include "version.h" + +const char version_and_features[] = + "version: " FOOT_VERSION + +#if defined(FOOT_PGO_ENABLED) && FOOT_PGO_ENABLED + " +pgo" +#else + " -pgo" +#endif + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + " +ime" +#else + " -ime" +#endif + +#if defined(FOOT_GRAPHEME_CLUSTERING) && FOOT_GRAPHEME_CLUSTERING + " +graphemes" +#else + " -graphemes" +#endif + +#if defined(HAVE_XDG_TOPLEVEL_TAG) + " +toplevel-tag" +#else + " -toplevel-tag" +#endif + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + " +blur" +#else + " -blur" +#endif + +#if !defined(NDEBUG) + " +assertions" +#else + " -assertions" +#endif +; diff --git a/foot-features.h b/foot-features.h new file mode 100644 index 0000000..49cc56e --- /dev/null +++ b/foot-features.h @@ -0,0 +1,13 @@ +#pragma once + +#include <stdio.h> + +extern const char version_and_features[]; + +static inline void +print_version_and_features(const char *prefix) +{ + fputs(prefix, stdout); + fputs(version_and_features, stdout); + fputc('\n', stdout); +} diff --git a/foot-server.desktop b/foot-server.desktop new file mode 100644 index 0000000..6e8891c --- /dev/null +++ b/foot-server.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Application +Exec=foot --server +Icon=foot +Terminal=false +Categories=System;TerminalEmulator; +Keywords=shell;prompt;command;commandline; + +Name=Foot Server +GenericName=Terminal +Comment=A wayland native terminal emulator (server) diff --git a/foot-server.service.in b/foot-server.service.in new file mode 100644 index 0000000..118b19a --- /dev/null +++ b/foot-server.service.in @@ -0,0 +1,15 @@ +[Service] +ExecStart=@bindir@/foot --server=3 +UnsetEnvironment=LISTEN_PID LISTEN_FDS LISTEN_FDNAMES +NonBlocking=true + +[Unit] +Requires=%N.socket +Description=Foot terminal server mode +Documentation=man:foot(1) +PartOf=graphical-session.target +After=graphical-session.target +ConditionEnvironment=WAYLAND_DISPLAY + +[Install] +WantedBy=graphical-session.target diff --git a/foot-server.socket b/foot-server.socket new file mode 100644 index 0000000..0c7c1b8 --- /dev/null +++ b/foot-server.socket @@ -0,0 +1,10 @@ +[Socket] +ListenStream=%t/foot.sock + +[Unit] +PartOf=graphical-session.target +After=graphical-session.target +ConditionEnvironment=WAYLAND_DISPLAY + +[Install] +WantedBy=graphical-session.target diff --git a/foot.desktop b/foot.desktop new file mode 100644 index 0000000..f072568 --- /dev/null +++ b/foot.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Application +Exec=foot +Icon=foot +Terminal=false +Categories=System;TerminalEmulator; +Keywords=shell;prompt;command;commandline; + +Name=Foot +GenericName=Terminal +Comment=A wayland native terminal emulator diff --git a/foot.info b/foot.info new file mode 100644 index 0000000..13f4403 --- /dev/null +++ b/foot.info @@ -0,0 +1,285 @@ +@default_terminfo@|foot terminal emulator, + use=@default_terminfo@+base, + colors#256, + setab=\E[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48\:5\:%p1%d%;m, + setaf=\E[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38\:5\:%p1%d%;m, + +@default_terminfo@-direct|foot with direct color indexing, + use=@default_terminfo@+base, + colors#16777216, + RGB, + setab=\E[%?%p1%{8}%<%t4%p1%d%e48\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m, + setaf=\E[%?%p1%{8}%<%t3%p1%d%e38\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m, + +@default_terminfo@+base|foot base fragment, + AX, + Su, + Tc, + XF, + XT, + am, + bce, + bw, + ccc, + hs, + mir, + msgr, + npc, + xenl, + cols#80, + it#8, + lines#24, + pairs#0x10000, + BD=\E[?2004l, + BE=\E[?2004h, + Cr=\E]112\E\\, + Cs=\E]12;%p1%s\E\\, + E3=\E[3J, + Ms=\E]52;%p1%s;%p2%s\E\\, + PE=\E[201~, + PS=\E[200~, + RV=\E[>c, + Rect=\E[%p1%d;%p2%d;%p3%d;%p4%d;%p5%d$x, + Se=\E[ q, + Setulc=\E[58\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m, + Smulx=\E[4:%p1%dm, + Ss=\E[%p1%d q, + Sync=\E[?2026%?%p1%{1}%-%tl%eh%;, + TS=\E]2;, + XM=\E[?1006;1000%?%p1%{1}%=%th%el%;, + XR=\E[>0q, + acsc=``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, + bel=^G, + blink=\E[5m, + bold=\E[1m, + cbt=\E[Z, + civis=\E[?25l, + clear=\E[H\E[2J, + cnorm=\E[?12l\E[?25h, + cr=\r, + csr=\E[%i%p1%d;%p2%dr, + cub1=^H, + cub=\E[%p1%dD, + cud1=\n, + cud=\E[%p1%dB, + cuf1=\E[C, + cuf=\E[%p1%dC, + cup=\E[%i%p1%d;%p2%dH, + cuu1=\E[A, + cuu=\E[%p1%dA, + cvvis=\E[?12;25h, + dch1=\E[P, + dch=\E[%p1%dP, + dim=\E[2m, + dl1=\E[M, + dl=\E[%p1%dM, + dsl=\E]2;\E\\, + ech=\E[%p1%dX, + ed=\E[J, + el1=\E[1K, + el=\E[K, + fd=\E[?1004l, + fe=\E[?1004h, + flash=\E]555\E\\, + fsl=\E\\, + home=\E[H, + hpa=\E[%i%p1%dG, + ht=^I, + hts=\EH, + ich=\E[%p1%d@, + il1=\E[L, + il=\E[%p1%dL, + ind=\n, + indn=\E[%p1%dS, + initc=\E]4;%p1%d;rgb\:%p2%{255}%*%{1000}%/%2.2X/%p3%{255}%*%{1000}%/%2.2X/%p4%{255}%*%{1000}%/%2.2X\E\\, + invis=\E[8m, + is2=\E[!p\E[4l\E>, + kDC3=\E[3;3~, + kDC4=\E[3;4~, + kDC5=\E[3;5~, + kDC6=\E[3;6~, + kDC7=\E[3;7~, + kDC=\E[3;2~, + kDN3=\E[1;3B, + kDN4=\E[1;4B, + kDN5=\E[1;5B, + kDN6=\E[1;6B, + kDN7=\E[1;7B, + kDN=\E[1;2B, + kEND3=\E[1;3F, + kEND4=\E[1;4F, + kEND5=\E[1;5F, + kEND6=\E[1;6F, + kEND7=\E[1;7F, + kEND=\E[1;2F, + kHOM3=\E[1;3H, + kHOM4=\E[1;4H, + kHOM5=\E[1;5H, + kHOM6=\E[1;6H, + kHOM7=\E[1;7H, + kHOM=\E[1;2H, + kIC3=\E[2;3~, + kIC4=\E[2;4~, + kIC5=\E[2;5~, + kIC6=\E[2;6~, + kIC7=\E[2;7~, + kIC=\E[2;2~, + kLFT3=\E[1;3D, + kLFT4=\E[1;4D, + kLFT5=\E[1;5D, + kLFT6=\E[1;6D, + kLFT7=\E[1;7D, + kLFT=\E[1;2D, + kNXT3=\E[6;3~, + kNXT4=\E[6;4~, + kNXT5=\E[6;5~, + kNXT6=\E[6;6~, + kNXT7=\E[6;7~, + kNXT=\E[6;2~, + kPRV3=\E[5;3~, + kPRV4=\E[5;4~, + kPRV5=\E[5;5~, + kPRV6=\E[5;6~, + kPRV7=\E[5;7~, + kPRV=\E[5;2~, + kRIT3=\E[1;3C, + kRIT4=\E[1;4C, + kRIT5=\E[1;5C, + kRIT6=\E[1;6C, + kRIT7=\E[1;7C, + kRIT=\E[1;2C, + kUP3=\E[1;3A, + kUP4=\E[1;4A, + kUP5=\E[1;5A, + kUP6=\E[1;6A, + kUP7=\E[1;7A, + kUP=\E[1;2A, + kbs=^?, + kcbt=\E[Z, + kcub1=\EOD, + kcud1=\EOB, + kcuf1=\EOC, + kcuu1=\EOA, + kdch1=\E[3~, + kend=\EOF, + kf10=\E[21~, + kf11=\E[23~, + kf12=\E[24~, + kf13=\E[1;2P, + kf14=\E[1;2Q, + kf15=\E[1;2R, + kf16=\E[1;2S, + kf17=\E[15;2~, + kf18=\E[17;2~, + kf19=\E[18;2~, + kf1=\EOP, + kf20=\E[19;2~, + kf21=\E[20;2~, + kf22=\E[21;2~, + kf23=\E[23;2~, + kf24=\E[24;2~, + kf25=\E[1;5P, + kf26=\E[1;5Q, + kf27=\E[1;5R, + kf28=\E[1;5S, + kf29=\E[15;5~, + kf2=\EOQ, + kf30=\E[17;5~, + kf31=\E[18;5~, + kf32=\E[19;5~, + kf33=\E[20;5~, + kf34=\E[21;5~, + kf35=\E[23;5~, + kf36=\E[24;5~, + kf37=\E[1;6P, + kf38=\E[1;6Q, + kf39=\E[1;6R, + kf3=\EOR, + kf40=\E[1;6S, + kf41=\E[15;6~, + kf42=\E[17;6~, + kf43=\E[18;6~, + kf44=\E[19;6~, + kf45=\E[20;6~, + kf46=\E[21;6~, + kf47=\E[23;6~, + kf48=\E[24;6~, + kf49=\E[1;3P, + kf4=\EOS, + kf50=\E[1;3Q, + kf51=\E[1;3R, + kf52=\E[1;3S, + kf53=\E[15;3~, + kf54=\E[17;3~, + kf55=\E[18;3~, + kf56=\E[19;3~, + kf57=\E[20;3~, + kf58=\E[21;3~, + kf59=\E[23;3~, + kf5=\E[15~, + kf60=\E[24;3~, + kf61=\E[1;4P, + kf62=\E[1;4Q, + kf63=\E[1;4R, + kf6=\E[17~, + kf7=\E[18~, + kf8=\E[19~, + kf9=\E[20~, + khome=\EOH, + kich1=\E[2~, + kind=\E[1;2B, + kmous=\E[<, + knp=\E[6~, + kpp=\E[5~, + kri=\E[1;2A, + kxIN=\E[I, + kxOUT=\E[O, + nel=\EE, + oc=\E]104\E\\, + op=\E[39;49m, + rc=\E8, + rep=%p1%c\E[%p2%{1}%-%db, + rev=\E[7m, + ri=\EM, + rin=\E[%p1%dT, + ritm=\E[23m, + rmacs=\E(B, + rmam=\E[?7l, + rmcup=\E[?1049l\E[23;0;0t, + rmir=\E[4l, + rmkx=\E[?1l\E>, + rmm=\E[?1036h\E[?1034l, + rmso=\E[27m, + rmul=\E[24m, + rmxx=\E[29m, + rs1=\Ec, + rs2=\E[!p\E[4l\E>, + rv=\E\\[>1;[0-9][0-9][0-9][0-9][0-9][0-9];0c, + sc=\E7, + setal=\E[58\:2\:\:%p1%{65536}%/%d\:%p1%{256}%/%{255}%&%d\:%p1%{255}%&%d%;m, + setrgbb=\E[48\:2\:\:%p1%d\:%p2%d\:%p3%dm, + setrgbf=\E[38\:2\:\:%p1%d\:%p2%d\:%p3%dm, + sgr0=\E(B\E[m, + sgr=%?%p9%t\E(0%e\E(B%;\E[0%?%p6%t;1%;%?%p5%t;2%;%?%p2%t;4%;%?%p1%p3%|%t;7%;%?%p4%t;5%;%?%p7%t;8%;m, + sitm=\E[3m, + smacs=\E(0, + smam=\E[?7h, + smcup=\E[?1049h\E[22;0;0t, + smir=\E[4h, + smkx=\E[?1h\E=, + smm=\E[?1036l\E[?1034h, + smso=\E[7m, + smul=\E[4m, + smxx=\E[9m, + tbc=\E[3g, + tsl=\E]2;, + u6=\E[%i%d;%dR, + u7=\E[6n, + u8=\E[?%[;0123456789]c, + u9=\E[c, + vpa=\E[%i%p1%dd, + xm=\E[<%i%p3%d;%p1%d;%p2%d;%?%p4%tM%em%;, + xr=\EP>\\|foot\\([0-9]+\\.[0-9]+\\.[0-9]+(-[0-9]+-g[a-f[0-9]+)?\\)?\E\\\\, + +# XT, +# AX, diff --git a/foot.ini b/foot.ini new file mode 100644 index 0000000..1722de0 --- /dev/null +++ b/foot.ini @@ -0,0 +1,319 @@ +# -*- conf -*- + +# shell=$SHELL (if set, otherwise user's default shell from /etc/passwd) +# term=foot (or xterm-256color if built with -Dterminfo=disabled) +# login-shell=no + +# app-id=foot # globally set wayland app-id. Default values are "foot" and "footclient" for desktop and server mode +# title=foot +# locked-title=no + +# font=monospace:size=8 +# font-bold=<bold variant of regular font> +# font-italic=<italic variant of regular font> +# font-bold-italic=<bold+italic variant of regular font> +# font-size-adjustment=0.5 +# line-height=<font metrics> +# letter-spacing=0 +# horizontal-letter-offset=0 +# vertical-letter-offset=0 +# underline-offset=<font metrics> +# underline-thickness=<font underline thickness> +# strikeout-thickness=<font strikeout thickness> +# box-drawings-uses-font-glyphs=no +# dpi-aware=no +# gamma-correct-blending=no + +# initial-color-theme=dark +# initial-window-size-pixels=700x500 # Or, +# initial-window-size-chars=<COLSxROWS> +# initial-window-mode=windowed +# pad=0x0 center-when-maximized-and-fullscreen +# resize-by-cells=yes +# resize-keep-grid=yes +# resize-delay-ms=100 + +# bold-text-in-bright=no +# word-delimiters=,│`|:"'()[]{}<> +# selection-target=primary +# workers=<number of logical CPUs> +# utmp-helper=/usr/lib/utempter/utempter # When utmp backend is ‘libutempter’ (Linux) +# utmp-helper=/usr/libexec/ulog-helper # When utmp backend is ‘ulog’ (FreeBSD) + +# uppercase-regex-insert=yes + +[environment] +# name=value + +[security] +# osc52=enabled # disabled|copy-enabled|paste-enabled|enabled + +[bell] +# system=yes +# urgent=no +# notify=no +# visual=no +# command= +# command-focused=no + +[desktop-notifications] +# command=notify-send --wait --app-name ${app-id} --icon ${app-id} --category ${category} --urgency ${urgency} --expire-time ${expire-time} --hint STRING:image-path:${icon} --hint BOOLEAN:suppress-sound:${muted} --hint STRING:sound-name:${sound-name} --replace-id ${replace-id} ${action-argument} --print-id -- ${title} ${body} +# command-action-argument=--action ${action-name}=${action-label} +# close="" +# inhibit-when-focused=yes + + +[scrollback] +# lines=1000 +# multiplier=3.0 +# indicator-position=relative +# indicator-format="" + +[url] +# launch=xdg-open ${url} +# label-letters=sadfjklewcmpgh +# style=dotted (none|single|double|curly|dotted|dashed) +# osc8-underline=url-mode +# regex=(((https?://|mailto:|ftp://|file:|ssh:|ssh://|git://|tel:|magnet:|ipfs://|ipns://|gemini://|gopher://|news:)|www\.)([0-9a-zA-Z:/?#@!$&*+,;=.~_%^\-]+|\([]\["0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\-]*')+([0-9a-zA-Z/#@$&*+=~_%^\-]|\([]\["0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\)|\[[\(\)"0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*\]|"[]\[\(\)0-9a-zA-Z:/?#@!$&'*+,;=.~_%^\-]*"|'[]\[\(\)0-9a-zA-Z:/?#@!$&*+,;=.~_%^\-]*')) + +# You can define your own regex's, by adding a section called +# 'regex:<ID>' with a 'regex' and 'launch' key. These can then be tied +# to a key-binding. See foot.ini(5) for details + +# [regex:your-fancy-name] +# regex=<a POSIX-Extended Regular Expression> +# launch=<path to script or application> ${match} +# +# [key-bindings] +# regex-launch=[your-fancy-name] Control+Shift+q +# regex-copy=[your-fancy-name] Control+Alt+Shift+q + +[cursor] +# style=block +# blink=no +# blink-rate=500 +# beam-thickness=1.5 +# underline-thickness=<font underline thickness> + +[mouse] +# hide-when-typing=no +# alternate-scroll-mode=yes + +[touch] +# long-press-delay=400 + +[colors-dark] +# alpha=1.0 +# alpha-mode=default # Can be `default`, `matching` or `all` +# background=242424 +# foreground=ffffff +# flash=7f7f00 +# flash-alpha=0.5 + +# cursor=<inverse foreground/background> + +## Normal/regular colors (color palette 0-7) +# regular0=242424 # black +# regular1=f62b5a # red +# regular2=47b413 # green +# regular3=e3c401 # yellow +# regular4=24acd4 # blue +# regular5=f2affd # magenta +# regular6=13c299 # cyan +# regular7=e6e6e6 # white + +## Bright colors (color palette 8-15) +# bright0=616161 # bright black +# bright1=ff4d51 # bright red +# bright2=35d450 # bright green +# bright3=e9e836 # bright yellow +# bright4=5dc5f8 # bright blue +# bright5=feabf2 # bright magenta +# bright6=24dfc4 # bright cyan +# bright7=ffffff # bright white + +## dimmed colors (see foot.ini(5) man page) +# dim-blend-towards=black +# dim0=<not set> +# ... +# dim7=<not-set> + +## The remaining 256-color palette +# 16 = <256-color palette #16> +# ... +# 255 = <256-color palette #255> + +## Sixel colors +# sixel0 = 000000 +# sixel1 = 3333cc +# sixel2 = cc2121 +# sixel3 = 33cc33 +# sixel4 = cc33cc +# sixel5 = 33cccc +# sixel6 = cccc33 +# sixel7 = 878787 +# sixel8 = 424242 +# sixel9 = 545499 +# sixel10 = 994242 +# sixel11 = 549954 +# sixel12 = 995499 +# sixel13 = 549999 +# sixel14 = 999954 +# sixel15 = cccccc + +## Misc colors +# selection-foreground=<inverse foreground/background> +# selection-background=<inverse foreground/background> +# jump-labels=<regular0> <regular3> # black-on-yellow +# scrollback-indicator=<regular0> <bright4> # black-on-bright-blue +# search-box-no-match=<regular0> <regular1> # black-on-red +# search-box-match=<regular0> <regular3> # black-on-yellow +# urls=<regular3> + +[colors-light] +# Alternative color theme, see man page foot.ini(5) +# Same builtin defaults as [color], except for: +# dim-blend-towards=white + +[tabs] +enabled=yes +position=bottom +style=rounded +layout=floating +height=26 +# tab-width=200 (max width per tab in floating mode) +# tab-padding=8 (gap between tabs in floating mode) +# label-padding=8 (horizontal padding around the label inside each tab pill) +# margin=4 (edge gap; auto-added to bar height, does not squish pill) +# corner-radius=6 (corner rounding in pixels) +# background=1c1c1c +# foreground=b0b0b0 +# active-background=3a3a3a +# active-foreground=ffffff +# inherit-cwd=no (new tabs open in the active tab's cwd; requires OSC 7 shell support) +# unread-indicator=● (string drawn before label when tab has unseen output; empty disables) +# unread-color=fabd2f (color of the unread-indicator) + +[csd] +# preferred=server +# size=26 +# font=<primary font> +# color=<foreground color> +# hide-when-maximized=no +# double-click-to-maximize=yes +# border-width=0 +# border-color=<csd.color> +# button-width=26 +# button-color=<background color> +# button-minimize-color=<regular4> +# button-maximize-color=<regular2> +# button-close-color=<regular1> + +[key-bindings] +# scrollback-up-page=Shift+Page_Up Shift+KP_Page_Up +# scrollback-up-half-page=none +# scrollback-up-line=none +# scrollback-down-page=Shift+Page_Down Shift+KP_Page_Down +# scrollback-down-half-page=none +# scrollback-down-line=none +# scrollback-home=none +# scrollback-end=none +# clipboard-copy=Control+Shift+c XF86Copy +# clipboard-paste=Control+Shift+v XF86Paste +# primary-paste=Shift+Insert +# search-start=Control+Shift+r +# font-increase=Control+plus Control+equal Control+KP_Add +# font-decrease=Control+minus Control+KP_Subtract +# font-reset=Control+0 Control+KP_0 +# spawn-terminal=Control+Shift+n +# minimize=none +# maximize=none +# fullscreen=none +# pipe-visible=[sh -c "xurls | fuzzel | xargs -r firefox"] none +# pipe-scrollback=[sh -c "xurls | fuzzel | xargs -r firefox"] none +# pipe-selected=[xargs -r firefox] none +# pipe-command-output=[wl-copy] none # Copy last command's output to the clipboard +# show-urls-launch=Control+Shift+o +# show-urls-copy=none +# show-urls-persistent=none +# prompt-prev=Control+Shift+z +# prompt-next=Control+Shift+x +# unicode-input=Control+Shift+u +# color-theme-switch-1=none +# color-theme-switch-2=none +# color-theme-toggle=none +# noop=none +# quit=none +# tab-new=Control+Shift+t +# tab-close=Control+Shift+w +# tab-next=Control+Tab +# tab-prev=Control+Shift+Tab +# tab-overview=Control+Shift+space + +[search-bindings] +# cancel=Control+g Control+c Escape +# commit=Return KP_Enter +# commit-line=Control+Return +# find-prev=Control+r +# find-next=Control+s +# toggle-case=Mod1+c +# toggle-whole-word=Mod1+w +# toggle-regex=Mod1+r +# history-prev=Up +# history-next=Down +# cursor-left=Left Control+b +# cursor-left-word=Control+Left Mod1+b +# cursor-right=Right Control+f +# cursor-right-word=Control+Right Mod1+f +# cursor-home=Home Control+a +# cursor-end=End Control+e +# delete-prev=BackSpace +# delete-prev-word=Mod1+BackSpace Control+BackSpace +# delete-next=Delete +# delete-next-word=Mod1+d Control+Delete +# delete-to-start=Control+u +# delete-to-end=Control+k +# extend-char=Shift+Right +# extend-to-word-boundary=Control+w Control+Shift+Right +# extend-to-next-whitespace=Control+Shift+w +# extend-line-down=Shift+Down +# extend-backward-char=Shift+Left +# extend-backward-to-word-boundary=Control+Shift+Left +# extend-backward-to-next-whitespace=none +# extend-line-up=Shift+Up +# clipboard-paste=Control+v Control+Shift+v Control+y XF86Paste +# primary-paste=Shift+Insert +# unicode-input=none +# scrollback-up-page=Shift+Page_Up Shift+KP_Page_Up +# scrollback-up-half-page=none +# scrollback-up-line=none +# scrollback-down-page=Shift+Page_Down Shift+KP_Page_Down +# scrollback-down-half-page=none +# scrollback-down-line=none +# scrollback-home=none +# scrollback-end=none + +[url-bindings] +# cancel=Control+g Control+c Control+d Escape +# toggle-url-visible=t + +[text-bindings] +# \x03=Mod4+c # Map Super+c -> Ctrl+c + +[mouse-bindings] +# scrollback-up-mouse=BTN_WHEEL_BACK +# scrollback-down-mouse=BTN_WHEEL_FORWARD +# font-increase=Control+BTN_WHEEL_BACK +# font-decrease=Control+BTN_WHEEL_FORWARD +# selection-override-modifiers=Shift +# primary-paste=BTN_MIDDLE +# select-begin=BTN_LEFT +# select-begin-block=Control+BTN_LEFT +# select-extend=BTN_RIGHT +# select-extend-character-wise=Control+BTN_RIGHT +# select-word=BTN_LEFT-2 +# select-word-whitespace=Control+BTN_LEFT-2 +# select-quote = BTN_LEFT-3 +# select-row=BTN_LEFT-4 + +# vim: ft=dosini diff --git a/footclient.desktop b/footclient.desktop new file mode 100644 index 0000000..f82f282 --- /dev/null +++ b/footclient.desktop @@ -0,0 +1,11 @@ +[Desktop Entry] +Type=Application +Exec=footclient +Icon=foot +Terminal=false +Categories=System;TerminalEmulator; +Keywords=shell;prompt;command;commandline; + +Name=Foot Client +GenericName=Terminal +Comment=A wayland native terminal emulator (client) diff --git a/generate-version.sh b/generate-version.sh new file mode 100755 index 0000000..a030d51 --- /dev/null +++ b/generate-version.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +set -e + +if [ ${#} -ne 3 ]; then + echo "Usage: ${0} <default_version> <src_dir> <out_file>" + exit 1 +fi + +default_version=${1} +src_dir=${2} +out_file=${3} + +# echo "default version: ${default_version}" +# echo "source directory: ${src_dir}" +# echo "output file: ${out_file}" + +if [ -d "${src_dir}/.git" ] && command -v git > /dev/null; then + workdir=$(pwd) + cd "${src_dir}" + + if git describe --tags > /dev/null 2>&1; then + git_version=$(git describe --always --tags) + else + # No tags available, happens in e.g. CI builds + git_version="${default_version}" + fi + + git_branch=$(git rev-parse --abbrev-ref HEAD) + cd "${workdir}" + + new_version="${git_version} ($(date "+%b %d %Y"), branch '${git_branch}')" +else + new_version="${default_version}" + extra="" +fi + +major=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\1/') +minor=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\2/') +patch=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\3/') +extra=$(echo "${new_version}" | sed -r 's/([0-9]+)\.([0-9]+)\.([0-9]+)(-([0-9]+-g[a-z0-9]+) .*)?.*/\5/') + +new_version="#define FOOT_VERSION \"${new_version}\" +#define FOOT_MAJOR ${major} +#define FOOT_MINOR ${minor} +#define FOOT_PATCH ${patch} +#define FOOT_EXTRA \"${extra}\"" + +if [ -f "${out_file}" ]; then + old_version=$(cat "${out_file}") +else + old_version="" +fi + +# echo "old version: ${old_version}" +# echo "new version: ${new_version}" + +if [ "${old_version}" != "${new_version}" ]; then + echo "${new_version}" > "${out_file}" +fi diff --git a/grid.c b/grid.c new file mode 100644 index 0000000..df7ef61 --- /dev/null +++ b/grid.c @@ -0,0 +1,1676 @@ +#include "grid.h" + +#include <limits.h> +#include <stdlib.h> +#include <string.h> + +#define LOG_MODULE "grid" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" +#include "macros.h" +#include "sixel.h" +#include "stride.h" +#include "util.h" +#include "xmalloc.h" + +#define TIME_REFLOW 0 + +#if defined(TIME_REFLOW) +#include "misc.h" +#endif + +/* + * "sb" (scrollback relative) coordinates + * + * The scrollback relative row number 0 is the *first*, and *oldest* + * row in the scrollback history (and thus the *first* row to be + * scrolled out). Thus, a higher number means further *down* in the + * scrollback, with the *highest* number being at the bottom of the + * screen, where new input appears. + */ + +int +grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row) +{ + const int scrollback_start = grid->offset + screen_rows; + int rebased_row = abs_row - scrollback_start + grid->num_rows; + + rebased_row &= grid->num_rows - 1; + return rebased_row; +} + +int +grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row) +{ + const int scrollback_start = grid->offset + screen_rows; + int abs_row = sb_rel_row + scrollback_start; + + abs_row &= grid->num_rows - 1; + return abs_row; +} + +int +grid_sb_start_ignore_uninitialized(const struct grid *grid, int screen_rows) +{ + int scrollback_start = grid->offset + screen_rows; + scrollback_start &= grid->num_rows - 1; + + while (grid->rows[scrollback_start] == NULL) { + scrollback_start++; + scrollback_start &= grid->num_rows - 1; + } + + return scrollback_start; +} + +int +grid_row_abs_to_sb_precalc_sb_start(const struct grid *grid, int sb_start, + int abs_row) +{ + int rebased_row = abs_row - sb_start + grid->num_rows; + rebased_row &= grid->num_rows - 1; + return rebased_row; +} + +int +grid_row_sb_to_abs_precalc_sb_start(const struct grid *grid, int sb_start, + int sb_rel_row) +{ + int abs_row = sb_rel_row + sb_start; + abs_row &= grid->num_rows - 1; + return abs_row; +} + +static void +ensure_row_has_extra_data(struct row *row) +{ + if (row->extra == NULL) + row->extra = xcalloc(1, sizeof(*row->extra)); +} + +static void +verify_no_overlapping_ranges_of_type(const struct row_ranges *ranges, + enum row_range_type type) +{ +#if defined(_DEBUG) + for (size_t i = 0; i < ranges->count; i++) { + const struct row_range *r1 = &ranges->v[i]; + + for (size_t j = i + 1; j < ranges->count; j++) { + const struct row_range *r2 = &ranges->v[j]; + xassert(r1 != r2); + + if ((r1->start <= r2->start && r1->end >= r2->start) || + (r1->start <= r2->end && r1->end >= r2->end)) + { + switch (type) { + case ROW_RANGE_URI: + BUG("OSC-8 URI overlap: %s: %d-%d: %s: %d-%d", + r1->uri.uri, r1->start, r1->end, + r2->uri.uri, r2->start, r2->end); + break; + + case ROW_RANGE_UNDERLINE: + BUG("underline overlap: %d-%d, %d-%d", + r1->start, r1->end, r2->start, r2->end); + break; + } + } + } + } +#endif +} + +static void +verify_no_overlapping_ranges(const struct row_data *extra) +{ + verify_no_overlapping_ranges_of_type(&extra->uri_ranges, ROW_RANGE_URI); + verify_no_overlapping_ranges_of_type(&extra->underline_ranges, ROW_RANGE_UNDERLINE); +} + +static void +verify_ranges_of_type_are_sorted(const struct row_ranges *ranges, + enum row_range_type type) +{ +#if defined(_DEBUG) + const struct row_range *last = NULL; + + for (size_t i = 0; i < ranges->count; i++) { + const struct row_range *r = &ranges->v[i]; + + if (last != NULL) { + if (last->start >= r->start || last->end >= r->end) { + switch (type) { + case ROW_RANGE_URI: + BUG("OSC-8 URI not sorted correctly: " + "%s: %d-%d came before %s: %d-%d", + last->uri.uri, last->start, last->end, + r->uri.uri, r->start, r->end); + break; + + case ROW_RANGE_UNDERLINE: + BUG("underline ranges not sorted correctly: " + "%d-%d came before %d-%d", + last->start, last->end, r->start, r->end); + break; + } + } + } + + last = r; + } +#endif +} + +static void +verify_ranges_are_sorted(const struct row_data *extra) +{ + verify_ranges_of_type_are_sorted(&extra->uri_ranges, ROW_RANGE_URI); + verify_ranges_of_type_are_sorted(&extra->underline_ranges, ROW_RANGE_UNDERLINE); +} + +static void +range_ensure_size(struct row_ranges *ranges, int count_to_add) +{ + if (ranges->count + count_to_add > ranges->size) { + ranges->size = ranges->count + count_to_add; + ranges->v = xrealloc(ranges->v, ranges->size * sizeof(ranges->v[0])); + } + + xassert(ranges->count + count_to_add <= ranges->size); +} + +/* + * Be careful! This function may xrealloc() the URI range vector, thus + * invalidating pointers into it. + */ +static void +range_insert(struct row_ranges *ranges, size_t idx, int start, int end, + enum row_range_type type, const union row_range_data *data) +{ + range_ensure_size(ranges, 1); + + xassert(idx <= ranges->count); + + const size_t move_count = ranges->count - idx; + memmove(&ranges->v[idx + 1], + &ranges->v[idx], + move_count * sizeof(ranges->v[0])); + + ranges->count++; + + struct row_range *r = &ranges->v[idx]; + r->start = start; + r->end = end; + + switch (type) { + case ROW_RANGE_URI: + r->uri.id = data->uri.id; + r->uri.uri = xstrdup(data->uri.uri); + break; + + case ROW_RANGE_UNDERLINE: + r->underline = data->underline; + break; + } +} + +static void +range_append_by_ref(struct row_ranges *ranges, int start, int end, + enum row_range_type type, const union row_range_data *data) +{ + range_ensure_size(ranges, 1); + + struct row_range *r = &ranges->v[ranges->count++]; + + r->start = start; + r->end = end; + + switch (type) { + case ROW_RANGE_URI: + r->uri.id = data->uri.id;; + r->uri.uri = data->uri.uri; + break; + + case ROW_RANGE_UNDERLINE: + r->underline = data->underline; + break; + } +} + +static void +range_append(struct row_ranges *ranges, int start, int end, + enum row_range_type type, const union row_range_data *data) +{ + switch (type) { + case ROW_RANGE_URI: + range_append_by_ref( + ranges, start, end, type, + &(union row_range_data){.uri = {.id = data->uri.id, + .uri = xstrdup(data->uri.uri)}}); + break; + + case ROW_RANGE_UNDERLINE: + range_append_by_ref(ranges, start, end, type, data); + break; + } +} + +static void +range_delete(struct row_ranges *ranges, enum row_range_type type, size_t idx) +{ + xassert(idx < ranges->count); + grid_row_range_destroy(&ranges->v[idx], type); + + const size_t move_count = ranges->count - idx - 1; + memmove(&ranges->v[idx], + &ranges->v[idx + 1], + move_count * sizeof(ranges->v[0])); + ranges->count--; +} + +struct grid * +grid_snapshot(const struct grid *grid) +{ + struct grid *clone = xmalloc(sizeof(*clone)); + clone->num_rows = grid->num_rows; + clone->num_cols = grid->num_cols; + clone->offset = grid->offset; + clone->view = grid->view; + clone->cursor = grid->cursor; + clone->saved_cursor = grid->saved_cursor; + clone->kitty_kbd = grid->kitty_kbd; + clone->rows = xcalloc(grid->num_rows, sizeof(clone->rows[0])); + memset(&clone->scroll_damage, 0, sizeof(clone->scroll_damage)); + memset(&clone->sixel_images, 0, sizeof(clone->sixel_images)); + + tll_foreach(grid->scroll_damage, it) + tll_push_back(clone->scroll_damage, it->item); + + for (int r = 0; r < grid->num_rows; r++) { + const struct row *row = grid->rows[r]; + + if (row == NULL) + continue; + + struct row *clone_row = xmalloc(sizeof(*row)); + clone->rows[r] = clone_row; + + clone_row->cells = xmalloc(grid->num_cols * sizeof(clone_row->cells[0])); + clone_row->linebreak = row->linebreak; + clone_row->dirty = row->dirty; + clone_row->shell_integration = row->shell_integration; + + for (int c = 0; c < grid->num_cols; c++) + clone_row->cells[c] = row->cells[c]; + + const struct row_data *extra = row->extra; + + if (extra != NULL) { + struct row_data *clone_extra = xcalloc(1, sizeof(*clone_extra)); + clone_row->extra = clone_extra; + + range_ensure_size(&clone_extra->uri_ranges, extra->uri_ranges.count); + range_ensure_size(&clone_extra->underline_ranges, extra->underline_ranges.count); + + for (int i = 0; i < extra->uri_ranges.count; i++) { + const struct row_range *range = &extra->uri_ranges.v[i]; + range_append( + &clone_extra->uri_ranges, + range->start, range->end, ROW_RANGE_URI, &range->data); + } + + for (int i = 0; i < extra->underline_ranges.count; i++) { + const struct row_range *range = &extra->underline_ranges.v[i]; + range_append_by_ref( + &clone_extra->underline_ranges, range->start, range->end, + ROW_RANGE_UNDERLINE, &range->data); + } + } else + clone_row->extra = NULL; + } + + tll_foreach(grid->sixel_images, it) { + int original_width = it->item.original.width; + int original_height = it->item.original.height; + pixman_image_t *original_pix = it->item.original.pix; + pixman_format_code_t original_pix_fmt = pixman_image_get_format(original_pix); + int original_stride = stride_for_format_and_width(original_pix_fmt, original_width); + + size_t original_size = original_stride * original_height; + void *new_original_data = xmemdup(it->item.original.data, original_size); + + pixman_image_t *new_original_pix = pixman_image_create_bits_no_clear( + original_pix_fmt, original_width, original_height, + new_original_data, original_stride); + + void *new_scaled_data = NULL; + pixman_image_t *new_scaled_pix = NULL; + int scaled_width = -1; + int scaled_height = -1; + + if (it->item.scaled.data != NULL) { + scaled_width = it->item.scaled.width; + scaled_height = it->item.scaled.height; + + pixman_image_t *scaled_pix = it->item.scaled.pix; + pixman_format_code_t scaled_pix_fmt = pixman_image_get_format(scaled_pix); + int scaled_stride = stride_for_format_and_width(scaled_pix_fmt, scaled_width); + + size_t scaled_size = scaled_stride * scaled_height; + new_scaled_data = xmemdup(it->item.scaled.data, scaled_size); + + new_scaled_pix = pixman_image_create_bits_no_clear( + scaled_pix_fmt, scaled_width, scaled_height, new_scaled_data, + scaled_stride); + } + + struct sixel six = { + .pix = (it->item.pix == it->item.original.pix + ? new_original_pix + : (it->item.pix == it->item.scaled.pix + ? new_scaled_pix + : NULL)), + .width = it->item.width, + .height = it->item.height, + .rows = it->item.rows, + .cols = it->item.cols, + .pos = it->item.pos, + .opaque = it->item.opaque, + .cell_width = it->item.cell_width, + .cell_height = it->item.cell_height, + .original = { + .data = new_original_data, + .pix = new_original_pix, + .width = original_width, + .height = original_height, + }, + .scaled = { + .data = new_scaled_data, + .pix = new_scaled_pix, + .width = scaled_width, + .height = scaled_height, + }, + }; + + tll_push_back(clone->sixel_images, six); + } + + return clone; +} + +void +grid_free(struct grid *grid) +{ + if (grid == NULL) + return; + + for (int r = 0; r < grid->num_rows; r++) + grid_row_free(grid->rows[r]); + + tll_foreach(grid->sixel_images, it) { + sixel_destroy(&it->item); + tll_remove(grid->sixel_images, it); + } + + free(grid->rows); + tll_free(grid->scroll_damage); +} + +void +grid_swap_row(struct grid *grid, int row_a, int row_b) +{ + xassert(grid->offset >= 0); + xassert(row_a != row_b); + + int real_a = (grid->offset + row_a) & (grid->num_rows - 1); + int real_b = (grid->offset + row_b) & (grid->num_rows - 1); + + struct row *a = grid->rows[real_a]; + struct row *b = grid->rows[real_b]; + + grid->rows[real_a] = b; + grid->rows[real_b] = a; +} + +struct row * +grid_row_alloc(int cols, bool initialize) +{ + struct row *row = xmalloc(sizeof(*row)); + row->dirty = false; + row->linebreak = true; + row->extra = NULL; + row->shell_integration.prompt_marker = false; + row->shell_integration.cmd_start = -1; + row->shell_integration.cmd_end = -1; + + if (initialize) { + row->cells = xcalloc(cols, sizeof(row->cells[0])); + for (size_t c = 0; c < cols; c++) + row->cells[c].attrs.clean = 1; + } else + row->cells = xmalloc(cols * sizeof(row->cells[0])); + + return row; +} + +void +grid_row_free(struct row *row) +{ + if (row == NULL) + return; + + grid_row_reset_extra(row); + free(row->extra); + free(row->cells); + free(row); +} + +void +grid_resize_without_reflow( + struct grid *grid, int new_rows, int new_cols, + int old_screen_rows, int new_screen_rows) +{ + struct row *const *old_grid = grid->rows; + const int old_rows = grid->num_rows; + const int old_cols = grid->num_cols; + + struct row **new_grid = xcalloc(new_rows, sizeof(new_grid[0])); + + tll(struct sixel) untranslated_sixels = tll_init(); + tll_foreach(grid->sixel_images, it) + tll_push_back(untranslated_sixels, it->item); + tll_free(grid->sixel_images); + + int new_offset = 0; + + /* Copy old lines, truncating them if old rows were longer */ + for (int r = 0, n = min(old_screen_rows, new_screen_rows); r < n; r++) { + const int old_row_idx = (grid->offset + r) & (old_rows - 1); + const int new_row_idx = (new_offset + r) & (new_rows - 1); + + const struct row *old_row = old_grid[old_row_idx]; + xassert(old_row != NULL); + + struct row *new_row = grid_row_alloc(new_cols, false); + new_grid[new_row_idx] = new_row; + + memcpy(new_row->cells, + old_row->cells, + sizeof(struct cell) * min(old_cols, new_cols)); + + new_row->dirty = old_row->dirty; + new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker; + new_row->shell_integration.cmd_start = min(old_row->shell_integration.cmd_start, new_cols - 1); + new_row->shell_integration.cmd_end = min(old_row->shell_integration.cmd_end, new_cols - 1); + + if (new_cols > old_cols) { + /* Clear "new" columns */ + memset(&new_row->cells[old_cols], 0, + sizeof(struct cell) * (new_cols - old_cols)); + new_row->dirty = true; + } else if (old_cols > new_cols) { + /* Make sure we don't cut a multi-column character in two */ + for (int i = new_cols; i > 0 && old_row->cells[i].wc > CELL_SPACER; i--) + new_row->cells[i - 1].wc = 0; + } + + /* Map sixels on current "old" row to current "new row" */ + tll_foreach(untranslated_sixels, it) { + if (it->item.pos.row != old_row_idx) + continue; + + struct sixel sixel = it->item; + sixel.pos.row = new_row_idx; + + if (sixel.pos.col < new_cols) + tll_push_back(grid->sixel_images, sixel); + else + sixel_destroy(&it->item); + tll_remove(untranslated_sixels, it); + } + + /* Copy URI ranges, truncating them if necessary */ + const struct row_data *old_extra = old_row->extra; + if (old_extra == NULL) + continue; + + ensure_row_has_extra_data(new_row); + struct row_data *new_extra = new_row->extra; + + range_ensure_size(&new_extra->uri_ranges, old_extra->uri_ranges.count); + range_ensure_size(&new_extra->underline_ranges, old_extra->underline_ranges.count); + + for (int i = 0; i < old_extra->uri_ranges.count; i++) { + const struct row_range *range = &old_extra->uri_ranges.v[i]; + + if (range->start >= new_cols) { + /* The whole range is truncated */ + continue; + } + + const int start = range->start; + const int end = min(range->end, new_cols - 1); + range_append(&new_extra->uri_ranges, start, end, ROW_RANGE_URI, &range->data); + } + + for (int i = 0; i < old_extra->underline_ranges.count; i++) { + const struct row_range *range = &old_extra->underline_ranges.v[i]; + + if (range->start >= new_cols) { + /* The whole range is truncated */ + continue; + } + + const int start = range->start; + const int end = min(range->end, new_cols - 1); + range_append_by_ref(&new_extra->underline_ranges, start, end, ROW_RANGE_UNDERLINE, &range->data); + } +} + + /* Clear "new" lines */ + for (int r = min(old_screen_rows, new_screen_rows); r < new_screen_rows; r++) { + struct row *new_row = grid_row_alloc(new_cols, false); + new_grid[(new_offset + r) & (new_rows - 1)] = new_row; + + memset(new_row->cells, 0, sizeof(struct cell) * new_cols); + new_row->dirty = true; + } + +#if defined(_DEBUG) + for (size_t r = 0; r < new_rows; r++) { + const struct row *row = new_grid[r]; + + if (row == NULL) + continue; + if (row->extra == NULL) + continue; + + verify_no_overlapping_ranges(row->extra); + verify_ranges_are_sorted(row->extra); + } +#endif + + /* Free old grid */ + for (int r = 0; r < grid->num_rows; r++) + grid_row_free(old_grid[r]); + free(grid->rows); + + grid->rows = new_grid; + grid->num_rows = new_rows; + grid->num_cols = new_cols; + + grid->view = grid->offset = new_offset; + + /* Keep cursor at current position, but clamp to new dimensions */ + struct coord cursor = grid->cursor.point; + if (cursor.row == old_screen_rows - 1) { + /* 'less' breaks if the cursor isn't at the bottom */ + cursor.row = new_screen_rows - 1; + } + cursor.row = min(cursor.row, new_screen_rows - 1); + cursor.col = min(cursor.col, new_cols - 1); + grid->cursor.point = cursor; + + struct coord saved_cursor = grid->saved_cursor.point; + if (saved_cursor.row == old_screen_rows - 1) + saved_cursor.row = new_screen_rows - 1; + saved_cursor.row = min(saved_cursor.row, new_screen_rows - 1); + saved_cursor.col = min(saved_cursor.col, new_cols - 1); + grid->saved_cursor.point = saved_cursor; + + grid->cur_row = new_grid[(grid->offset + cursor.row) & (new_rows - 1)]; + xassert(grid->cur_row != NULL); + + grid->cursor.lcf = false; + grid->saved_cursor.lcf = false; + + /* Free sixels we failed to "map" to the new grid */ + tll_foreach(untranslated_sixels, it) + sixel_destroy(&it->item); + tll_free(untranslated_sixels); + +#if defined(_DEBUG) + for (int r = 0; r < new_screen_rows; r++) + grid_row_in_view(grid, r); +#endif +} + +static void +reflow_range_start(struct row_range *range, enum row_range_type type, + struct row *new_row, int new_col_idx) +{ + ensure_row_has_extra_data(new_row); + + struct row_ranges *new_ranges = NULL; + switch (type) { + case ROW_RANGE_URI: new_ranges = &new_row->extra->uri_ranges; break; + case ROW_RANGE_UNDERLINE: new_ranges = &new_row->extra->underline_ranges; break; + } + + if (new_ranges == NULL) + BUG("unhandled range type"); + + range_append_by_ref(new_ranges, new_col_idx, -1, type, &range->data); + + switch (type) { + case ROW_RANGE_URI: range->uri.uri = NULL; break; /* Owned by new_ranges */ + case ROW_RANGE_UNDERLINE: break; + } +} + +static void +reflow_range_end(struct row_range *range, enum row_range_type type, + struct row *new_row, int new_col_idx) +{ + struct row_data *extra = new_row->extra; + struct row_ranges *ranges = NULL; + + switch (type) { + case ROW_RANGE_URI: ranges = &extra->uri_ranges; break; + case ROW_RANGE_UNDERLINE: ranges = &extra->underline_ranges; break; + } + + if (ranges == NULL) + BUG("unhandled range type"); + + xassert(ranges->count > 0); + + struct row_range *new_range = &ranges->v[ranges->count - 1]; + xassert(new_range->end < 0); + + switch (type) { + case ROW_RANGE_URI: + xassert(new_range->uri.id == range->uri.id); + break; + + case ROW_RANGE_UNDERLINE: + xassert(new_range->underline.style == range->underline.style); + xassert(new_range->underline.color_src == range->underline.color_src); + xassert(new_range->underline.color == range->underline.color); + break; + } + + new_range->end = new_col_idx; +} + +static struct row * +_line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row, + int *row_idx, int *col_idx, int row_count, int col_count) +{ + *col_idx = 0; + *row_idx = (*row_idx + 1) & (row_count - 1); + + struct row *new_row = new_grid[*row_idx]; + + if (new_row == NULL) { + /* Scrollback not yet full, allocate a completely new row */ + new_row = grid_row_alloc(col_count, false); + new_grid[*row_idx] = new_row; + } else { + /* Scrollback is full, need to reuse a row */ + grid_row_reset_extra(new_row); + new_row->shell_integration.prompt_marker = false; + new_row->shell_integration.cmd_start = -1; + new_row->shell_integration.cmd_end = -1; + + tll_foreach(old_grid->sixel_images, it) { + if (it->item.pos.row == *row_idx) { + sixel_destroy(&it->item); + tll_remove(old_grid->sixel_images, it); + } + } + + /* + * TODO: detect if the reused row is covered by the + * selection. Of so, cancel the selection. The problem: we + * don't know if we've translated the selection coordinates + * yet. + */ + } + + struct row_data *extra = row->extra; + if (extra == NULL) + return new_row; + + /* + * URI ranges are per row. Thus, we need to 'close' the still-open + * ranges on the previous row, and re-open them on the + * next/current row. + */ + if (extra->uri_ranges.count > 0) { + struct row_range *range = + &extra->uri_ranges.v[extra->uri_ranges.count - 1]; + + if (range->end < 0) { + + /* Terminate URI range on the previous row */ + range->end = col_count - 1; + + /* Open a new range on the new/current row */ + ensure_row_has_extra_data(new_row); + range_append(&new_row->extra->uri_ranges, 0, -1, + ROW_RANGE_URI, &range->data); + } + } + + if (extra->underline_ranges.count > 0) { + struct row_range *range = + &extra->underline_ranges.v[extra->underline_ranges.count - 1]; + + if (range->end < 0) { + + /* Terminate URI range on the previous row */ + range->end = col_count - 1; + + /* Open a new range on the new/current row */ + ensure_row_has_extra_data(new_row); + range_append(&new_row->extra->underline_ranges, 0, -1, + ROW_RANGE_UNDERLINE, &range->data); + } + } + + return new_row; +} + +static struct { + int scrollback_start; + int rows; +} tp_cmp_ctx; + +static int +tp_cmp(const void *_a, const void *_b) +{ + const struct coord *a = *(const struct coord **)_a; + const struct coord *b = *(const struct coord **)_b; + + int scrollback_start = tp_cmp_ctx.scrollback_start; + int num_rows = tp_cmp_ctx.rows; + + int a_row = (a->row - scrollback_start + num_rows) & (num_rows - 1); + int b_row = (b->row - scrollback_start + num_rows) & (num_rows - 1); + + xassert(a_row >= 0); + xassert(a_row < num_rows || num_rows == 0); + xassert(b_row >= 0); + xassert(b_row < num_rows || num_rows == 0); + + if (a_row < b_row) + return -1; + if (a_row > b_row) + return 1; + + xassert(a_row == b_row); + + if (a->col < b->col) + return -1; + if (a->col > b->col) + return 1; + + xassert(a->col == b->col); + return 0; +} + +void +grid_resize_and_reflow( + struct grid *grid, const struct terminal *term, int new_rows, int new_cols, + int old_screen_rows, int new_screen_rows, + size_t tracking_points_count, + struct coord *const _tracking_points[static tracking_points_count]) +{ +#if defined(TIME_REFLOW) && TIME_REFLOW + struct timespec start; + clock_gettime(CLOCK_MONOTONIC, &start); +#endif + + struct row *const *old_grid = grid->rows; + const int old_rows = grid->num_rows; + const int old_cols = grid->num_cols; + + /* Is viewpoint tracking current grid offset? */ + const bool view_follows = grid->view == grid->offset; + + int new_col_idx = 0; + int new_row_idx = 0; + + struct row **new_grid = xcalloc(new_rows, sizeof(new_grid[0])); + struct row *new_row = new_grid[new_row_idx]; + + xassert(new_row == NULL); + new_row = grid_row_alloc(new_cols, false); + new_grid[new_row_idx] = new_row; + + /* Start at the beginning of the old grid's scrollback. That is, + * at the output that is *oldest* */ + int offset = grid->offset + old_screen_rows; + + tll(struct sixel) untranslated_sixels = tll_init(); + tll_foreach(grid->sixel_images, it) + tll_push_back(untranslated_sixels, it->item); + tll_free(grid->sixel_images); + + /* Turn cursor coordinates into grid absolute coordinates */ + struct coord cursor = grid->cursor.point; + cursor.row += grid->offset; + cursor.row &= old_rows - 1; + + struct coord saved_cursor = grid->saved_cursor.point; + saved_cursor.row += grid->offset; + saved_cursor.row &= old_rows - 1; + + size_t tp_count = + tracking_points_count + + 1 + /* cursor */ + 1 + /* saved cursor */ + !view_follows + /* viewport */ + 1; /* terminator */ + + struct coord *tracking_points[tp_count]; + memcpy(tracking_points, _tracking_points, tracking_points_count * sizeof(_tracking_points[0])); + tracking_points[tracking_points_count] = &cursor; + tracking_points[tracking_points_count + 1] = &saved_cursor; + + struct coord viewport = {0, grid->view}; + if (!view_follows) + tracking_points[tracking_points_count + 2] = &viewport; + + /* Not thread safe! */ + tp_cmp_ctx.scrollback_start = offset; + tp_cmp_ctx.rows = old_rows; + qsort( + tracking_points, tp_count - 1, sizeof(tracking_points[0]), &tp_cmp); + + /* NULL terminate */ + struct coord terminator = {-1, -1}; + tracking_points[tp_count - 1] = &terminator; + struct coord **next_tp = &tracking_points[0]; + + LOG_DBG("scrollback-start=%d", offset); + for (size_t i = 0; i < tp_count - 1; i++) { + LOG_DBG("TP #%zu: row=%d, col=%d", + i, tracking_points[i]->row, tracking_points[i]->col); + } + + int coalesced_linebreaks = 0; + + /* + * Walk the old grid + */ + for (int r = 0; r < old_rows; r++) { + + const size_t old_row_idx = (offset + r) & (old_rows - 1); + + /* Unallocated (empty) rows we can simply skip */ + const struct row *old_row = old_grid[old_row_idx]; + if (old_row == NULL) + continue; + + /* Map sixels on current "old" row to current "new row" */ + tll_foreach(untranslated_sixels, it) { + if (it->item.pos.row != old_row_idx) + continue; + + struct sixel sixel = it->item; + sixel.pos.row = new_row_idx; + + tll_push_back(grid->sixel_images, sixel); + tll_remove(untranslated_sixels, it); + } + +#define line_wrap() \ + new_row = _line_wrap( \ + grid, new_grid, new_row, &new_row_idx, &new_col_idx, \ + new_rows, new_cols) + + /* Find last non-empty cell */ + int col_count = 0; + for (int c = old_cols - 1; c >= 0; c--) { + const struct cell *cell = &old_row->cells[c]; + if (!(cell->wc == 0 || cell->wc == CELL_SPACER)) { + col_count = c + 1; + break; + } + } + + if (!old_row->linebreak && col_count > 0) { + /* Don't truncate logical lines */ + while (col_count < old_cols && old_row->cells[col_count].wc == 0) + col_count++; + } + + xassert(col_count >= 0 && col_count <= old_cols); + + /* Do we have a (at least one) tracking point on this row */ + struct coord *tp; + if (unlikely((*next_tp)->row == old_row_idx)) { + tp = *next_tp; + + /* Find the *last* tracking point on this row */ + struct coord *last_on_row = tp; + for (struct coord **iter = next_tp; (*iter)->row == old_row_idx; iter++) + last_on_row = *iter; + + /* And make sure its end point is included in the col range */ + xassert(last_on_row->row == old_row_idx); + col_count = max(col_count, last_on_row->col + 1); + } else + tp = NULL; + + /* Does this row have any URIs? */ + struct row_range *uri_range, *uri_range_terminator; + struct row_range *underline_range, *underline_range_terminator; + const struct row_data *extra = old_row->extra; + + if (extra != NULL && extra->uri_ranges.count > 0) { + uri_range = &extra->uri_ranges.v[0]; + uri_range_terminator = &extra->uri_ranges.v[extra->uri_ranges.count]; + + /* Make sure the *last* URI range's end point is included + * in the copy */ + const struct row_range *last_on_row = + &extra->uri_ranges.v[extra->uri_ranges.count - 1]; + col_count = max(col_count, last_on_row->end + 1); + } else + uri_range = uri_range_terminator = NULL; + + if (extra != NULL && extra->underline_ranges.count > 0) { + underline_range = &extra->underline_ranges.v[0]; + underline_range_terminator = &extra->underline_ranges.v[extra->underline_ranges.count]; + + const struct row_range *last_on_row = + &extra->underline_ranges.v[extra->underline_ranges.count - 1]; + col_count = max(col_count, last_on_row->end + 1); + } else + underline_range = underline_range_terminator = NULL; + + if (unlikely(col_count > 0 && coalesced_linebreaks > 0)) { + for (size_t line_no = 0; line_no < coalesced_linebreaks; line_no++) { + /* Erase the remaining cells */ + memset(&new_row->cells[new_col_idx], 0, + (new_cols - new_col_idx) * sizeof(new_row->cells[0])); + new_row->linebreak = true; + line_wrap(); + } + + coalesced_linebreaks = 0; + } + + for (int c = 0; c < col_count;) { + const struct cell *old = &old_row->cells[c]; + + /* Row full, emit newline and get a new, fresh, row */ + xassert(new_col_idx <= new_cols); + if (unlikely(new_col_idx >= new_cols)) + line_wrap(); + + char32_t wc = old->wc; + int width = 1; + + if (unlikely(wc >= CELL_COMB_CHARS_LO && wc <= CELL_COMB_CHARS_HI)) { + const struct composed *composed = + composed_lookup(term->composed, wc - CELL_COMB_CHARS_LO); + + width = composed->forced_width > 0 ? composed->forced_width : composed->width; + } else if (unlikely(c + 1 < col_count && (old + 1)->wc >= CELL_SPACER + 1)) { + /* Wide character, get its width from the next cell's + SPACER value */ + width = (old + 1)->wc - CELL_SPACER + 1; + } + + /* + * Check if character fits, if not, emit spacers, and push + the character to the next row */ + if (unlikely(new_col_idx + width > new_cols && width <= new_cols)) { + for (; new_col_idx < new_cols; new_col_idx++) { + new_row->cells[new_col_idx].wc = CELL_SPACER; + new_row->cells[new_col_idx].attrs = (struct attributes){0}; + } + line_wrap(); + } + + new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker; + + for (int i = 0; i < width; i++) { + if (unlikely(uri_range != NULL && uri_range != uri_range_terminator)) { + if (unlikely(uri_range->start == c)) { + reflow_range_start( + uri_range, ROW_RANGE_URI, new_row, new_col_idx); + } + + if (unlikely(uri_range->end == c)) { + reflow_range_end( + uri_range, ROW_RANGE_URI, new_row, new_col_idx); + grid_row_uri_range_destroy(uri_range); + uri_range++; + } + } + + if (unlikely(underline_range != NULL && underline_range != underline_range_terminator)) { + if (unlikely(underline_range->start == c)) { + reflow_range_start( + underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx); + } + + if (unlikely(underline_range->end == c)) { + reflow_range_end( + underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx); + grid_row_underline_range_destroy(underline_range); + underline_range++; + } + } + + if (unlikely(tp != NULL)) { + if (unlikely(tp->col == c)) { + do { + xassert(tp->row == old_row_idx); + + tp->row = new_row_idx; + tp->col = new_col_idx; + + next_tp++; + tp = *next_tp; + } while (tp->row == old_row_idx && tp->col == c); + + if (tp->row != old_row_idx) + tp = NULL; + + LOG_DBG("next TP (tp=%p): %dx%d", + (void*)tp, (*next_tp)->row, (*next_tp)->col); + } + } + + if (unlikely(old_row->shell_integration.cmd_start == c)) + new_row->shell_integration.cmd_start = new_col_idx; + + if (unlikely(old_row->shell_integration.cmd_end == c)) + new_row->shell_integration.cmd_end = new_col_idx; + + if (unlikely(width > new_cols)) { + /* Wide character no longer fits on a row, replace + it with a single space */ + new_row->cells[new_col_idx++].wc = 0; + c++; + + /* Walk past the SPACER cells */ + for (int i = 1; i < width; i++, c++, old++) + ; + + /* Continue with next character in the *old* grid */ + break; + } + + new_row->cells[new_col_idx++] = *old; + + /* + * TODO: simulate LCF instead? + * + * Rows have linebreak=true by default. This is needed + * for a number of reasons. However, we want non-empty + * rows to have linebreak=false, *until* we reach the + * end of an old row with linebreak=true, at which + * point we set linebreak=true on the new row. + */ + new_row->linebreak = false; + old++; + c++; + } + } + + if (old_row->linebreak) { + if (col_count > 0) { + /* Erase the remaining cells */ + memset(&new_row->cells[new_col_idx], 0, + (new_cols - new_col_idx) * sizeof(new_row->cells[0])); + new_row->linebreak = true; + + if (r + 1 < old_rows) { + /* Not the last (old) row */ + line_wrap(); + } else if (new_row->extra != NULL) { + if (new_row->extra->uri_ranges.count > 0) { + /* + * line_wrap() "closes" still-open URIs. Since + * this is the *last* row, and since we're + * line-breaking due to a hard line-break (rather + * than running out of cells in the "new_row"), + * there shouldn't be an open URI (it would have + * been closed when we reached the end of the URI + * while reflowing the last "old" row). + */ + int last_idx = new_row->extra->uri_ranges.count - 1; + xassert(new_row->extra->uri_ranges.v[last_idx].end >= 0); + } + + if (new_row->extra->underline_ranges.count > 0) { + int last_idx = new_row->extra->underline_ranges.count - 1; + xassert(new_row->extra->underline_ranges.v[last_idx].end >= 0); + } + } + } else { + /* + * rows have linebreak=true by default. But we don't + * want trailing empty lines to result in actual lines + * in the new grid (think: empty window with prompt at + * the top) + */ + coalesced_linebreaks++; + } + } + + grid_row_free(old_grid[old_row_idx]); + grid->rows[old_row_idx] = NULL; + +#undef line_wrap + } + + /* Erase the remaining cells */ + memset(&new_row->cells[new_col_idx], 0, + (new_cols - new_col_idx) * sizeof(new_row->cells[0])); + + for (struct coord **tp = next_tp; *tp != &terminator; tp++) { + LOG_DBG("TP: row=%d, col=%d (old cols: %d, new cols: %d)", + (*tp)->row, (*tp)->col, old_cols, new_cols); + } + xassert(old_rows == 0 || *next_tp == &terminator); + +#if defined(_DEBUG) + /* Verify all URI ranges have been "closed" */ + for (int r = 0; r < new_rows; r++) { + const struct row *row = new_grid[r]; + + if (row == NULL) + continue; + if (row->extra == NULL) + continue; + + for (size_t i = 0; i < row->extra->uri_ranges.count; i++) + xassert(row->extra->uri_ranges.v[i].end >= 0); + for (size_t i = 0; i < row->extra->underline_ranges.count; i++) + xassert(row->extra->underline_ranges.v[i].end >= 0); + + verify_no_overlapping_ranges(row->extra); + verify_ranges_are_sorted(row->extra); + } + + /* Verify all old rows have been free:d */ + for (int i = 0; i < old_rows; i++) + xassert(grid->rows[i] == NULL); +#endif + + /* Set offset such that the last reflowed row is at the bottom */ + grid->offset = new_row_idx - new_screen_rows + 1; + + while (grid->offset < 0) + grid->offset += new_rows; + while (new_grid[grid->offset] == NULL) + grid->offset = (grid->offset + 1) & (new_rows - 1); + + /* Ensure all visible rows have been allocated */ + for (int r = 0; r < new_screen_rows; r++) { + int idx = (grid->offset + r) & (new_rows - 1); + if (new_grid[idx] == NULL) + new_grid[idx] = grid_row_alloc(new_cols, true); + } + + /* Free old grid (rows already free:d) */ + free(grid->rows); + + grid->rows = new_grid; + grid->num_rows = new_rows; + grid->num_cols = new_cols; + + /* + * Set new viewport, making sure it's not too far down. + * + * This is done by using scrollback-start relative cooardinates, + * and bounding the new viewport to (grid_rows - screen_rows). + */ + int sb_view = grid_row_abs_to_sb( + grid, new_screen_rows, view_follows ? grid->offset : viewport.row); + grid->view = grid_row_sb_to_abs( + grid, new_screen_rows, min(sb_view, new_rows - new_screen_rows)); + + /* Convert absolute coordinates to screen relative */ + cursor.row -= grid->offset; + while (cursor.row < 0) + cursor.row += grid->num_rows; + cursor.row = min(cursor.row, new_screen_rows - 1); + cursor.col = min(cursor.col, new_cols - 1); + + saved_cursor.row -= grid->offset; + while (saved_cursor.row < 0) + saved_cursor.row += grid->num_rows; + saved_cursor.row = min(saved_cursor.row, new_screen_rows - 1); + saved_cursor.col = min(saved_cursor.col, new_cols - 1); + + if (grid->cursor.lcf) { + if (cursor.col + 1 < new_cols) { + cursor.col++; + grid->cursor.lcf = false; + } + } + + if (grid->saved_cursor.lcf) { + if (saved_cursor.col + 1 < new_cols) { + saved_cursor.col++; + grid->saved_cursor.lcf = false; + } + } + + grid->cur_row = new_grid[(grid->offset + cursor.row) & (new_rows - 1)]; + xassert(grid->cur_row != NULL); + + grid->cursor.point = cursor; + grid->saved_cursor.point = saved_cursor; + + /* Free sixels we failed to "map" to the new grid */ + tll_foreach(untranslated_sixels, it) + sixel_destroy(&it->item); + tll_free(untranslated_sixels); + +#if defined(TIME_REFLOW) && TIME_REFLOW + struct timespec stop; + clock_gettime(CLOCK_MONOTONIC, &stop); + + struct timespec diff; + timespec_sub(&stop, &start, &diff); + LOG_INFO("reflowed %d -> %d rows in %lds %ldns", + old_rows, new_rows, + (long)diff.tv_sec, + diff.tv_nsec); +#endif +} + +static bool +ranges_match(const struct row_range *r1, const struct row_range *r2, + enum row_range_type type) +{ + switch (type) { + case ROW_RANGE_URI: + /* TODO: also match URI? */ + return r1->uri.id == r2->uri.id; + + case ROW_RANGE_UNDERLINE: + return r1->underline.style == r2->underline.style && + r1->underline.color_src == r2->underline.color_src && + r1->underline.color == r2->underline.color; + } + + BUG("invalid range type"); + return false; +} + +static bool +range_match_data(const struct row_range *r, const union row_range_data *data, + enum row_range_type type) +{ + switch (type) { + case ROW_RANGE_URI: + return r->uri.id == data->uri.id; + + case ROW_RANGE_UNDERLINE: + return r->underline.style == data->underline.style && + r->underline.color_src == data->underline.color_src && + r->underline.color == data->underline.color; + } + + BUG("invalid range type"); + return false; +} + +static void +grid_row_range_put(struct row_ranges *ranges, int col, + const union row_range_data *data, enum row_range_type type) +{ + size_t insert_idx = 0; + bool replace = false; + bool run_merge_pass = false; + + for (int i = ranges->count - 1; i >= 0; i--) { + struct row_range *r = &ranges->v[i]; + + const bool matching = range_match_data(r, data, type); + + if (matching && r->end + 1 == col) { + /* Extend existing range tail */ + r->end++; + return; + } + + else if (r->end < col) { + insert_idx = i + 1; + break; + } + + else if (r->start > col) + continue; + + else { + xassert(r->start <= col); + xassert(r->end >= col); + + if (matching) + return; + + if (r->start == r->end) { + replace = true; + run_merge_pass = true; + insert_idx = i; + } else if (r->start == col) { + run_merge_pass = true; + r->start++; + insert_idx = i; + } else if (r->end == col) { + run_merge_pass = true; + r->end--; + insert_idx = i + 1; + } else { + xassert(r->start < col); + xassert(r->end > col); + + union row_range_data insert_data; + switch (type) { + case ROW_RANGE_URI: insert_data.uri = r->uri; break; + case ROW_RANGE_UNDERLINE: insert_data.underline = r->underline; break; + } + + range_insert(ranges, i + 1, col + 1, r->end, type, &insert_data); + + /* The insertion may xrealloc() the vector, making our + * 'old' pointer invalid */ + r = &ranges->v[i]; + r->end = col - 1; + xassert(r->start <= r->end); + + insert_idx = i + 1; + } + + break; + } + } + + xassert(insert_idx <= ranges->count); + + if (replace) { + grid_row_range_destroy(&ranges->v[insert_idx], type); + ranges->v[insert_idx] = (struct row_range){ + .start = col, + .end = col, + }; + + switch (type) { + case ROW_RANGE_URI: + ranges->v[insert_idx].uri.id = data->uri.id; + ranges->v[insert_idx].uri.uri = xstrdup(data->uri.uri); + break; + + case ROW_RANGE_UNDERLINE: + ranges->v[insert_idx].underline = data->underline; + break; + } + } else + range_insert(ranges, insert_idx, col, col, type, data); + + if (run_merge_pass) { + for (size_t i = 1; i < ranges->count; i++) { + struct row_range *r1 = &ranges->v[i - 1]; + struct row_range *r2 = &ranges->v[i]; + + if (ranges_match(r1, r2, type) && r1->end + 1 == r2->start) { + r1->end = r2->end; + range_delete(ranges, type, i); + i--; + } + } + } +} + +void +grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id) +{ + ensure_row_has_extra_data(row); + + grid_row_range_put( + &row->extra->uri_ranges, col, + &(union row_range_data){.uri = {.id = id, .uri = (char *)uri}}, + ROW_RANGE_URI); + + verify_no_overlapping_ranges(row->extra); + verify_ranges_are_sorted(row->extra); +} + +void +grid_row_underline_range_put(struct row *row, int col, struct underline_range_data data) +{ + ensure_row_has_extra_data(row); + + grid_row_range_put( + &row->extra->underline_ranges, col, + &(union row_range_data){.underline = data}, + ROW_RANGE_UNDERLINE); + + verify_no_overlapping_ranges(row->extra); + verify_ranges_are_sorted(row->extra); +} + +UNITTEST +{ + struct row_data row_data = {.uri_ranges = {0}}; + struct row row = {.extra = &row_data}; + +#define verify_range(idx, _start, _end, _id) \ + do { \ + xassert(idx < row_data.uri_ranges.count); \ + xassert(row_data.uri_ranges.v[idx].start == _start); \ + xassert(row_data.uri_ranges.v[idx].end == _end); \ + xassert(row_data.uri_ranges.v[idx].uri.id == _id); \ + } while (0) + + grid_row_uri_range_put(&row, 0, "http://foo.bar", 123); + grid_row_uri_range_put(&row, 1, "http://foo.bar", 123); + grid_row_uri_range_put(&row, 2, "http://foo.bar", 123); + grid_row_uri_range_put(&row, 3, "http://foo.bar", 123); + xassert(row_data.uri_ranges.count == 1); + verify_range(0, 0, 3, 123); + + /* No-op */ + grid_row_uri_range_put(&row, 0, "http://foo.bar", 123); + xassert(row_data.uri_ranges.count == 1); + verify_range(0, 0, 3, 123); + + /* Replace head */ + grid_row_uri_range_put(&row, 0, "http://head", 456); + xassert(row_data.uri_ranges.count == 2); + verify_range(0, 0, 0, 456); + verify_range(1, 1, 3, 123); + + /* Replace tail */ + grid_row_uri_range_put(&row, 3, "http://tail", 789); + xassert(row_data.uri_ranges.count == 3); + verify_range(1, 1, 2, 123); + verify_range(2, 3, 3, 789); + + /* Replace tail + extend head */ + grid_row_uri_range_put(&row, 2, "http://tail", 789); + xassert(row_data.uri_ranges.count == 3); + verify_range(1, 1, 1, 123); + verify_range(2, 2, 3, 789); + + /* Replace + extend tail */ + grid_row_uri_range_put(&row, 1, "http://head", 456); + xassert(row_data.uri_ranges.count == 2); + verify_range(0, 0, 1, 456); + verify_range(1, 2, 3, 789); + + /* Replace + extend, then splice */ + grid_row_uri_range_put(&row, 1, "http://tail", 789); + grid_row_uri_range_put(&row, 2, "http://splice", 000); + xassert(row_data.uri_ranges.count == 4); + verify_range(0, 0, 0, 456); + verify_range(1, 1, 1, 789); + verify_range(2, 2, 2, 000); + verify_range(3, 3, 3, 789); + + for (size_t i = 0; i < row_data.uri_ranges.count; i++) + grid_row_uri_range_destroy(&row_data.uri_ranges.v[i]); + free(row_data.uri_ranges.v); + +#undef verify_range +} + +static void +grid_row_range_erase(struct row_ranges *ranges, enum row_range_type type, + int start, int end) +{ + xassert(start <= end); + + /* Split up, or remove, URI ranges affected by the erase */ + for (int i = ranges->count - 1; i >= 0; i--) { + struct row_range *old = &ranges->v[i]; + + if (old->end < start) + return; + + if (old->start > end) + continue; + + if (start <= old->start && end >= old->end) { + /* Erase range covers URI completely - remove it */ + range_delete(ranges, type, i); + } + + else if (start > old->start && end < old->end) { + /* + * Erase range erases a part in the middle of the URI + * + * Must copy, since range_insert() may xrealloc() (thus + * causing 'old' to be invalid) before it dereferences + * old->data + */ + union row_range_data data = old->data; + range_insert(ranges, i + 1, end + 1, old->end, type, &data); + + /* The insertion may xrealloc() the vector, making our + * 'old' pointer invalid */ + old = &ranges->v[i]; + old->end = start - 1; + return; /* There can be no more URIs affected by the erase range */ + } + + else if (start <= old->start && end >= old->start) { + /* Erase range erases the head of the URI */ + xassert(start <= old->start); + old->start = end + 1; + } + + else if (start <= old->end && end >= old->end) { + /* Erase range erases the tail of the URI */ + xassert(end >= old->end); + old->end = start - 1; + return; /* There can be no more overlapping URIs */ + } + } +} + +void +grid_row_uri_range_erase(struct row *row, int start, int end) +{ + xassert(row->extra != NULL); + grid_row_range_erase(&row->extra->uri_ranges, ROW_RANGE_URI, start, end); +} + +void +grid_row_underline_range_erase(struct row *row, int start, int end) +{ + xassert(row->extra != NULL); + grid_row_range_erase(&row->extra->underline_ranges, ROW_RANGE_UNDERLINE, start, end); +} + +UNITTEST +{ + struct row_data row_data = {.uri_ranges = {0}}; + struct row row = {.extra = &row_data}; + const union row_range_data data = { + .uri = { + .id = 0, + .uri = (char *)"dummy", + }, + }; + + /* Try erasing a row without any URIs */ + grid_row_uri_range_erase(&row, 0, 200); + xassert(row_data.uri_ranges.count == 0); + + range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); + range_append(&row_data.uri_ranges, 11, 20, ROW_RANGE_URI, &data); + xassert(row_data.uri_ranges.count == 2); + xassert(row_data.uri_ranges.v[1].start == 11); + xassert(row_data.uri_ranges.v[1].end == 20); + verify_no_overlapping_ranges(&row_data); + verify_ranges_are_sorted(&row_data); + + /* Erase both URis */ + grid_row_uri_range_erase(&row, 1, 20); + xassert(row_data.uri_ranges.count == 0); + verify_no_overlapping_ranges(&row_data); + verify_ranges_are_sorted(&row_data); + + /* Two URIs, then erase second half of the first, first half of + the second */ + range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); + range_append(&row_data.uri_ranges, 11, 20, ROW_RANGE_URI, &data); + grid_row_uri_range_erase(&row, 5, 15); + xassert(row_data.uri_ranges.count == 2); + xassert(row_data.uri_ranges.v[0].start == 1); + xassert(row_data.uri_ranges.v[0].end == 4); + xassert(row_data.uri_ranges.v[1].start == 16); + xassert(row_data.uri_ranges.v[1].end == 20); + verify_no_overlapping_ranges(&row_data); + verify_ranges_are_sorted(&row_data); + + grid_row_range_destroy(&row_data.uri_ranges.v[0], ROW_RANGE_URI); + grid_row_range_destroy(&row_data.uri_ranges.v[1], ROW_RANGE_URI); + row_data.uri_ranges.count = 0; + + /* One URI, erase middle part of it */ + range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); + grid_row_uri_range_erase(&row, 5, 6); + xassert(row_data.uri_ranges.count == 2); + xassert(row_data.uri_ranges.v[0].start == 1); + xassert(row_data.uri_ranges.v[0].end == 4); + xassert(row_data.uri_ranges.v[1].start == 7); + xassert(row_data.uri_ranges.v[1].end == 10); + verify_no_overlapping_ranges(&row_data); + verify_ranges_are_sorted(&row_data); + + grid_row_range_destroy(&row_data.uri_ranges.v[0], ROW_RANGE_URI); + grid_row_range_destroy(&row_data.uri_ranges.v[1], ROW_RANGE_URI); + row_data.uri_ranges.count = 0; + + /* + * Regression test: erasing the middle part of an URI causes us to + * insert a new URI (we split the partly erased URI into two). + * + * The insertion logic typically triggers an xrealloc(), which, in + * some cases, *moves* the entire URI vector to a new base + * address. grid_row_uri_range_erase() did not account for this, + * and tried to update the 'end' member in the URI range we just + * split. This causes foot to crash when the xrealloc() has moved + * the URI range vector. + * + * (note: we're only verifying we don't crash here, hence the lack + * of assertions). + */ + free(row_data.uri_ranges.v); + row_data.uri_ranges.v = NULL; + row_data.uri_ranges.size = 0; + range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data); + xassert(row_data.uri_ranges.size == 1); + + grid_row_uri_range_erase(&row, 5, 7); + xassert(row_data.uri_ranges.count == 2); + + grid_row_ranges_destroy(&row_data.uri_ranges, ROW_RANGE_URI); + free(row_data.uri_ranges.v); +} diff --git a/grid.h b/grid.h new file mode 100644 index 0000000..71bdc29 --- /dev/null +++ b/grid.h @@ -0,0 +1,138 @@ +#pragma once + +#include <stddef.h> +#include "debug.h" +#include "terminal.h" + +struct grid *grid_snapshot(const struct grid *grid); +void grid_free(struct grid *grid); + +void grid_swap_row(struct grid *grid, int row_a, int row_b); +struct row *grid_row_alloc(int cols, bool initialize); +void grid_row_free(struct row *row); + +void grid_resize_without_reflow( + struct grid *grid, int new_rows, int new_cols, + int old_screen_rows, int new_screen_rows); + +void grid_resize_and_reflow( + struct grid *grid, const struct terminal *term, int new_rows, int new_cols, + int old_screen_rows, int new_screen_rows, + size_t tracking_points_count, + struct coord *const _tracking_points[static tracking_points_count]); + +/* Convert row numbers between scrollback-relative and absolute coordinates */ +int grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row); +int grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row); + +int grid_sb_start_ignore_uninitialized(const struct grid *grid, int screen_rows); +int grid_row_abs_to_sb_precalc_sb_start( + const struct grid *grid, int sb_start, int abs_row); +int grid_row_sb_to_abs_precalc_sb_start( + const struct grid *grid, int sb_start, int sb_rel_row); + +static inline int +grid_row_absolute(const struct grid *grid, int row_no) +{ + return (grid->offset + row_no) & (grid->num_rows - 1); +} + +static inline int +grid_row_absolute_in_view(const struct grid *grid, int row_no) +{ + return (grid->view + row_no) & (grid->num_rows - 1); +} + +static inline struct row * +_grid_row_maybe_alloc(struct grid *grid, int row_no, bool alloc_if_null) +{ + xassert(grid->offset >= 0); + + int real_row = grid_row_absolute(grid, row_no); + struct row *row = grid->rows[real_row]; + + if (row == NULL && alloc_if_null) { + row = grid_row_alloc(grid->num_cols, false); + grid->rows[real_row] = row; + } + + xassert(row != NULL); + return row; +} + +static inline struct row * +grid_row(struct grid *grid, int row_no) +{ + return _grid_row_maybe_alloc(grid, row_no, false); +} + +static inline struct row * +grid_row_and_alloc(struct grid *grid, int row_no) +{ + return _grid_row_maybe_alloc(grid, row_no, true); +} + +static inline struct row * +grid_row_in_view(struct grid *grid, int row_no) +{ + xassert(grid->view >= 0); + + int real_row = grid_row_absolute_in_view(grid, row_no); + struct row *row = grid->rows[real_row]; + + xassert(row != NULL); + return row; +} + +void grid_row_uri_range_put( + struct row *row, int col, const char *uri, uint64_t id); +void grid_row_uri_range_erase(struct row *row, int start, int end); + +void grid_row_underline_range_put( + struct row *row, int col, struct underline_range_data data); +void grid_row_underline_range_erase(struct row *row, int start, int end); + +static inline void +grid_row_uri_range_destroy(struct row_range *range) +{ + free(range->uri.uri); +} + +static inline void +grid_row_underline_range_destroy(struct row_range *range) +{ +} + +static inline void +grid_row_range_destroy(struct row_range *range, enum row_range_type type) +{ + switch (type) { + case ROW_RANGE_URI: grid_row_uri_range_destroy(range); break; + case ROW_RANGE_UNDERLINE: grid_row_underline_range_destroy(range); break; + } +} + +static inline void +grid_row_ranges_destroy(struct row_ranges *ranges, enum row_range_type type) +{ + for (int i = 0; i < ranges->count; i++) { + grid_row_range_destroy(&ranges->v[i], type); + } +} + +static inline void +grid_row_reset_extra(struct row *row) +{ + struct row_data *extra = row->extra; + + if (likely(extra == NULL)) + return; + + grid_row_ranges_destroy(&extra->uri_ranges, ROW_RANGE_URI); + grid_row_ranges_destroy(&extra->underline_ranges, ROW_RANGE_UNDERLINE); + free(extra->uri_ranges.v); + free(extra->underline_ranges.v); + + free(extra); + row->extra = NULL; +} diff --git a/hsl.c b/hsl.c new file mode 100644 index 0000000..1a8c919 --- /dev/null +++ b/hsl.c @@ -0,0 +1,54 @@ +#include "hsl.h" + +#include <math.h> + +uint32_t +hsl_to_rgb(int hue, int sat, int lum) +{ + double L = lum / 100.0; + double S = sat / 100.0; + double C = (1. - fabs(2. * L - 1.)) * S; + + double X = C * (1. - fabs(fmod((double)hue / 60., 2.) - 1.)); + double m = L - C / 2.; + + double r, g, b; + if (hue >= 0 && hue <= 60) { + r = C; + g = X; + b = 0.; + } else if (hue >= 60 && hue <= 120) { + r = X; + g = C; + b = 0.; + } else if (hue >= 120 && hue <= 180) { + r = 0.; + g = C; + b = X; + } else if (hue >= 180 && hue <= 240) { + r = 0.; + g = X; + b = C; + } else if (hue >= 240 && hue <= 300) { + r = X; + g = 0.; + b = C; + } else if (hue >= 300 && hue <= 360) { + r = C; + g = 0.; + b = X; + } else { + r = 0.; + g = 0.; + b = 0.; + } + + r += m; + g += m; + b += m; + + return ( + (uint8_t)round(r * 255.) << 16 | + (uint8_t)round(g * 255.) << 8 | + (uint8_t)round(b * 255.) << 0); +} diff --git a/hsl.h b/hsl.h new file mode 100644 index 0000000..1aaf7e6 --- /dev/null +++ b/hsl.h @@ -0,0 +1,5 @@ +#pragma once + +#include <stdint.h> + +uint32_t hsl_to_rgb(int hue, int sat, int lum); diff --git a/icons/hicolor/48x48/apps/foot.png b/icons/hicolor/48x48/apps/foot.png new file mode 100644 index 0000000000000000000000000000000000000000..81fa20640b00e9618b39d9e1d3a394adca729e18 GIT binary patch literal 977 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIY)RhkE)4%caKYZ?lNlJ8 z+dN$yLoyoQ&Wz6yag{l4U%mb9&Kp{)7gi*!V)P8S?YyCLQSTOqBN+jc{sjspd-?Qa zEJ{f7{c`um0#47P5;>tgw~De3EIDc0u~w5Qcjd-#k=AQox0o*p_*&jtd;9NWX2G3z zcPDSVx!1VR-hcb&*>`L9ZJ#`Ir!)u0k9y@)ZJEyF;%CL$S357f!245Vld`0b=-IQf z@!Kmu%S*nVDOFv?XPOZC)}eEK*`t_E3wCZRdo2E~N%_FUikgXH6VI<$6?N&Nr1C@w zx6rG9GHwNkv}S~=`ug%}bWP$qJ+W)ntXcN!-wAKJza&<DJ*$_MRq~gwU&Vdo9703; zzJ9%YQ<7QZ>ev0(ZvD1kiV(ZbtRi$c^DTe=if3=l%LDeMEcD-XQ}WmKqlQbK=9u5y z|8siTmEzZ%iu~rzd$6r*cE;`NM^9v23JwgMzPfNvV-C+;iK7NSesks05C1Z~vvX5Z ztiG9&XQcY3Jrln2-l*LFH=C_*zKc1}HoKo}myI{C*}Z=f`@7w<Ctg%}u5Ns;XrHo( zLe$PV+sdOmT)G%$%)b26@{h87z0~rWLdorS_UuStY-tNPEc|-?31$(|lR2yQPWS(| z?_3kB;JeOy4`)BvD!}#O^@)mOWuB8N@_vim`c_)0^^|QP!~ElX7ws#Il5=lfQaw?0 zBUsa%gY`mb^IYAvf6U6R1#Z^izP!aoT-dcjYVV9$4_m{_7H3vP{k~Z&Zs2>l*f6k# znZZXzUDjFi<K-F4o$?Ar85ZOu{>u9I_hL)B(dUDq4aeoHC+rTpTlX~Ta>+hxn<qP` zNVkMP%I%e|{Q7kHrCYlmxzF5X@fIY?bj5Z{+x4<s@7v!c+D^L9KUym+?3%%0`}@q| zKHuB_*6%X*Tm94R1@D@OeI{?KQcO<1vwgY$UE6QFGe3LYp5K4;0RR3SS9Y5@F<K?v zzx3yeGQ*r7l|6HH*ZQ14v+Qf-`{riL`4er*6X$j96y*Bg_qr~Ly=uZ!gZ=X@`%^8$ z@7Y%KSLmrXeb6znGkm%H_@2VO6aHT3U=o?7;INPrL9JmFaXq=*-LGh;1(S!+WrmBZ zyyU0e*=QB@_55+~Z?lrkqik05X9-$fy>cahYdXI;SLKl{R#C3W$+iw&)0CUP*-8Gt z^ZwnxWpPbq3$CQQ2L>%mHNAc{_t&Mk6IE6hZ{Lni%Zl16`pm8VZjrL2Pit2<`<wP^ fMn=ZzKc(YSMQhdf>dj+dU|{fc^>bP0l+XkKE&<a= literal 0 HcmV?d00001 diff --git a/icons/hicolor/scalable/apps/foot.svg b/icons/hicolor/scalable/apps/foot.svg new file mode 100644 index 0000000..a87cdc9 --- /dev/null +++ b/icons/hicolor/scalable/apps/foot.svg @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="128" height="128" + inkscape:version="1.0 (4035a4fb49, 2020-05-01)"> + <title>foot logo + + + + + + + image/svg+xml + + + + Lennard Hofmann + + + https://freesvg.org/human-footprints + + foot logo + + + terminal emulator + footprint + + + 2020-06-23 + Black square representing a terminal showing a human footprint as a prompt symbol and an underscore as the cursor + + + + + + + + + + + + + + + + + diff --git a/icons/meson.build b/icons/meson.build new file mode 100644 index 0000000..8838206 --- /dev/null +++ b/icons/meson.build @@ -0,0 +1 @@ +install_subdir('hicolor', install_dir : join_paths(get_option('datadir'), 'icons')) diff --git a/ime.c b/ime.c new file mode 100644 index 0000000..c6ccb47 --- /dev/null +++ b/ime.c @@ -0,0 +1,525 @@ +#include "ime.h" + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + +#include + +#include "text-input-unstable-v3.h" + +#define LOG_MODULE "ime" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "char32.h" +#include "render.h" +#include "search.h" +#include "terminal.h" +#include "util.h" +#include "wayland.h" +#include "xmalloc.h" + +static void +ime_reset_pending_preedit(struct seat *seat) +{ + free(seat->ime.preedit.pending.text); + seat->ime.preedit.pending.text = NULL; +} + +static void +ime_reset_pending_commit(struct seat *seat) +{ + free(seat->ime.commit.pending.text); + seat->ime.commit.pending.text = NULL; +} + +void +ime_reset_pending(struct seat *seat) +{ + ime_reset_pending_preedit(seat); + ime_reset_pending_commit(seat); +} + +void +ime_reset_preedit(struct seat *seat) +{ + if (seat->ime.preedit.cells == NULL) + return; + + free(seat->ime.preedit.text); + free(seat->ime.preedit.cells); + seat->ime.preedit.text = NULL; + seat->ime.preedit.cells = NULL; + seat->ime.preedit.count = 0; +} + +static void +enter(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, + struct wl_surface *surface) +{ + struct seat *seat = data; + struct wl_window *win = wl_surface_get_user_data(surface); + struct terminal *term = win->term; + + LOG_DBG("enter: seat=%s, term=%p", seat->name, (const void *)term); + + if (seat->kbd_focus != term) { + LOG_WARN("compositor sent ime::enter() event before the " + "corresponding keyboard_enter() event"); + } + + /* The main grid is the *only* input-receiving surface we have */ + seat->ime_focus = term; + + const struct coord *cursor = &term->grid->cursor.point; + + term_ime_set_cursor_rect( + term, + term->margins.left + cursor->col * term->cell_width, + term->margins.top + cursor->row * term->cell_height, + term->cell_width, + term->cell_height); + + ime_enable(seat); +} + +static void +leave(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, + struct wl_surface *surface) +{ + struct seat *seat = data; + LOG_DBG("leave: seat=%s", seat->name); + + ime_disable(seat); + seat->ime_focus = NULL; +} + +static void +preedit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, + const char *text, int32_t cursor_begin, int32_t cursor_end) +{ + LOG_DBG("preedit-string: text=%s, begin=%d, end=%d", text, cursor_begin, cursor_end); + + struct seat *seat = data; + + ime_reset_pending_preedit(seat); + + if (text != NULL) { + seat->ime.preedit.pending.text = xstrdup(text); + seat->ime.preedit.pending.cursor_begin = cursor_begin; + seat->ime.preedit.pending.cursor_end = cursor_end; + } +} + +static void +commit_string(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, + const char *text) +{ + LOG_DBG("commit: text=%s", text); + + struct seat *seat = data; + + ime_reset_pending_commit(seat); + + if (text != NULL) + seat->ime.commit.pending.text = xstrdup(text); +} + +static void +delete_surrounding_text(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, + uint32_t before_length, uint32_t after_length) +{ + LOG_DBG("delete-surrounding: before=%d, after=%d", before_length, after_length); + + struct seat *seat = data; + seat->ime.surrounding.pending.before_length = before_length; + seat->ime.surrounding.pending.after_length = after_length; +} + +static void +done(void *data, struct zwp_text_input_v3 *zwp_text_input_v3, + uint32_t serial) +{ + /* + * From text-input-unstable-v3.h: + * + * The application must proceed by evaluating the changes in the + * following order: + * + * 1. Replace existing preedit string with the cursor. + * 2. Delete requested surrounding text. + * 3. Insert commit string with the cursor at its end. + * 4. Calculate surrounding text to send. + * 5. Insert new preedit text in cursor position. + * 6. Place cursor inside preedit text. + */ + + LOG_DBG("done: serial=%u", serial); + struct seat *seat = data; + struct terminal *term = seat->ime_focus; + + if (seat->ime.serial != serial) { + LOG_DBG("IME serial mismatch: expected=0x%08x, got 0x%08x", + seat->ime.serial, serial); + return; + } + + if (term == NULL) { + static bool have_warned = false; + if (!have_warned) { + LOG_WARN( + "%s: text-input::done() received on seat that isn't " + "focusing a terminal window", seat->name); + have_warned = true; + } + } + + /* 1. Delete existing pre-edit text */ + if (seat->ime.preedit.cells != NULL) { + ime_reset_preedit(seat); + + if (term != NULL) { + if (term->is_searching) + render_refresh_search(term); + else + render_refresh(term); + } + } + + /* + * 2. Delete requested surrounding text + * + * We don't support deleting surrounding text. But, we also never + * call set_surrounding_text() so hopefully we should never + * receive any requests to delete surrounding text. + */ + + /* 3. Insert commit string */ + if (seat->ime.commit.pending.text != NULL) { + const char *text = seat->ime.commit.pending.text; + size_t len = strlen(text); + + if (term != NULL) { + if (term->is_searching) { + search_add_chars(term, text, len); + render_refresh_search(term); + } else + term_to_slave(term, text, len); + } + ime_reset_pending_commit(seat); + } + + /* 4. Calculate surrounding text to send - not supported */ + + /* 5. Insert new pre-edit text */ + char32_t *allocated_preedit_text = NULL; + + if (seat->ime.preedit.pending.text == NULL || + seat->ime.preedit.pending.text[0] == '\0' || + (allocated_preedit_text = ambstoc32(seat->ime.preedit.pending.text)) == NULL) + { + ime_reset_pending_preedit(seat); + return; + } + + xassert(seat->ime.preedit.pending.text != NULL); + xassert(allocated_preedit_text != NULL); + + seat->ime.preedit.text = allocated_preedit_text; + + size_t wchars = c32len(seat->ime.preedit.text); + + /* Next, count number of cells needed */ + size_t cell_count = 0; + size_t widths[wchars + 1]; + + for (size_t i = 0; i < wchars; i++) { + int width = max(c32width(seat->ime.preedit.text[i]), 1); + widths[i] = width; + cell_count += width; + } + + /* Allocate cells */ + seat->ime.preedit.cells = xmalloc( + cell_count * sizeof(seat->ime.preedit.cells[0])); + seat->ime.preedit.count = cell_count; + + /* Populate cells */ + for (size_t i = 0, cell_idx = 0; i < wchars; i++) { + struct cell *cell = &seat->ime.preedit.cells[cell_idx]; + + int width = widths[i]; + + cell->wc = seat->ime.preedit.text[i]; + cell->attrs = (struct attributes){.clean = 0}; + + for (int j = 1; j < width; j++) { + cell = &seat->ime.preedit.cells[cell_idx + j]; + cell->wc = CELL_SPACER + width - j; + cell->attrs = (struct attributes){.clean = 1}; + } + + cell_idx += width; + } + + const size_t byte_len = strlen(seat->ime.preedit.pending.text); + + /* Pre-edit cursor - hidden */ + if (seat->ime.preedit.pending.cursor_begin == -1 || + seat->ime.preedit.pending.cursor_end == -1) + { + /* Note: docs says *both* begin and end should be -1, + * but what else can we do if only one is -1? */ + LOG_DBG("pre-edit cursor is hidden"); + seat->ime.preedit.cursor.hidden = true; + seat->ime.preedit.cursor.start = -1; + seat->ime.preedit.cursor.end = -1; + } + + else if (seat->ime.preedit.pending.cursor_begin == byte_len && + seat->ime.preedit.pending.cursor_end == byte_len) + { + /* Cursor is *after* the entire pre-edit string */ + seat->ime.preedit.cursor.hidden = false; + seat->ime.preedit.cursor.start = cell_count; + seat->ime.preedit.cursor.end = cell_count; + } + + else { + /* + * Translate cursor position to cell indices + * + * The cursor_begin and cursor_end are counted in + * *bytes*. We want to map them to *cell* indices. + * + * To do this, we use mblen() to step though the utf-8 + * pre-edit string, advancing a unicode character index as + * we go, *and* advancing a *cell* index using c32width() + * of the unicode character. + * + * When we find the matching *byte* index, we at the same + * time know both the unicode *and* cell index. + */ + + int cell_begin = -1, cell_end = -1; + for (size_t byte_idx = 0, wc_idx = 0, cell_idx = 0; + byte_idx < byte_len && + wc_idx < wchars && + cell_idx < cell_count && + (cell_begin < 0 || cell_end < 0); + cell_idx += widths[wc_idx], wc_idx++) + { + if (seat->ime.preedit.pending.cursor_begin == byte_idx) + cell_begin = cell_idx; + if (seat->ime.preedit.pending.cursor_end == byte_idx) + cell_end = cell_idx; + + /* Number of bytes of *next* utf-8 character */ + size_t left = byte_len - byte_idx; + int wc_bytes = mblen(&seat->ime.preedit.pending.text[byte_idx], left); + + if (wc_bytes <= 0) + break; + + byte_idx += wc_bytes; + } + + if (seat->ime.preedit.pending.cursor_end >= byte_len) + cell_end = cell_count; + + /* Bounded by number of screen columns */ + cell_begin = min(max(cell_begin, 0), cell_count - 1); + cell_end = min(max(cell_end, 0), cell_count); + + if (cell_end < cell_begin) + cell_end = cell_begin; + + /* Expand cursor end to end of glyph */ + while (cell_end > cell_begin && cell_end < cell_count && + seat->ime.preedit.cells[cell_end].wc >= CELL_SPACER) + { + cell_end++; + } + + LOG_DBG("pre-edit cursor: begin=%d, end=%d", cell_begin, cell_end); + + xassert(cell_begin >= 0); + xassert(cell_begin < cell_count); + xassert(cell_begin <= cell_end); + xassert(cell_end >= 0); + xassert(cell_end <= cell_count); + + seat->ime.preedit.cursor.hidden = false; + seat->ime.preedit.cursor.start = cell_begin; + seat->ime.preedit.cursor.end = cell_end; + } + + /* Underline pre-edit string that is *not* covered by the cursor */ + bool hidden = seat->ime.preedit.cursor.hidden; + int start = seat->ime.preedit.cursor.start; + int end = seat->ime.preedit.cursor.end; + + for (size_t i = 0, cell_idx = 0; i < wchars; cell_idx += widths[i], i++) { + if (hidden || start == end || cell_idx < start || cell_idx >= end) { + struct cell *cell = &seat->ime.preedit.cells[cell_idx]; + cell->attrs.underline = true; + } + } + + ime_reset_pending_preedit(seat); + + if (term != NULL) { + if (term->is_searching) + render_refresh_search(term); + else + render_refresh(term); + } +} + +static void +ime_send_cursor_rect(struct seat *seat) +{ + if (unlikely(seat->wayl->text_input_manager == NULL)) + return; + + if (seat->ime_focus == NULL) + return; + + struct terminal *term = seat->ime_focus; + + if (!term->ime_enabled) + return; + + if (seat->ime.cursor_rect.pending.x == seat->ime.cursor_rect.sent.x && + seat->ime.cursor_rect.pending.y == seat->ime.cursor_rect.sent.y && + seat->ime.cursor_rect.pending.width == seat->ime.cursor_rect.sent.width && + seat->ime.cursor_rect.pending.height == seat->ime.cursor_rect.sent.height) + { + return; + } + + zwp_text_input_v3_set_cursor_rectangle( + seat->wl_text_input, + seat->ime.cursor_rect.pending.x / term->scale, + seat->ime.cursor_rect.pending.y / term->scale, + seat->ime.cursor_rect.pending.width / term->scale, + seat->ime.cursor_rect.pending.height / term->scale); + + zwp_text_input_v3_commit(seat->wl_text_input); + seat->ime.serial++; + + seat->ime.cursor_rect.sent = seat->ime.cursor_rect.pending; +} + +void +ime_enable(struct seat *seat) +{ + if (unlikely(seat->wayl->text_input_manager == NULL)) + return; + + if (seat->ime_focus == NULL) + return; + + struct terminal *term = seat->ime_focus; + if (term == NULL) + return; + + if (!term->ime_enabled) + return; + + ime_reset_pending(seat); + ime_reset_preedit(seat); + + zwp_text_input_v3_enable(seat->wl_text_input); + zwp_text_input_v3_set_content_type( + seat->wl_text_input, + ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, + ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL); + + zwp_text_input_v3_set_cursor_rectangle( + seat->wl_text_input, + seat->ime.cursor_rect.pending.x / term->scale, + seat->ime.cursor_rect.pending.y / term->scale, + seat->ime.cursor_rect.pending.width / term->scale, + seat->ime.cursor_rect.pending.height / term->scale); + + seat->ime.cursor_rect.sent = seat->ime.cursor_rect.pending; + + zwp_text_input_v3_commit(seat->wl_text_input); + seat->ime.serial++; +} + +void +ime_disable(struct seat *seat) +{ + if (unlikely(seat->wayl->text_input_manager == NULL)) + return; + + if (seat->ime_focus == NULL) + return; + + ime_reset_pending(seat); + ime_reset_preedit(seat); + + zwp_text_input_v3_disable(seat->wl_text_input); + zwp_text_input_v3_commit(seat->wl_text_input); + seat->ime.serial++; +} + +void +ime_update_cursor_rect(struct seat *seat) +{ + struct terminal *term = seat->ime_focus; + + /* Set in render_ime_preedit() */ + if (seat->ime.preedit.cells != NULL) + goto update; + + /* Set in render_search_box() */ + if (term->is_searching) + goto update; + + int x, y, width, height; + int col = term->grid->cursor.point.col; + int row = term->grid->cursor.point.row; + row += term->grid->offset; + row -= term->grid->view; + row &= term->grid->num_rows - 1; + x = term->margins.left + col * term->cell_width; + y = term->margins.top + row * term->cell_height; + + if (term->cursor_style == CURSOR_BEAM) + width = 1; + else + width = term->cell_width; + + height = term->cell_height; + + seat->ime.cursor_rect.pending.x = x; + seat->ime.cursor_rect.pending.y = y; + seat->ime.cursor_rect.pending.width = width; + seat->ime.cursor_rect.pending.height = height; + +update: + ime_send_cursor_rect(seat); +} + +const struct zwp_text_input_v3_listener text_input_listener = { + .enter = &enter, + .leave = &leave, + .preedit_string = &preedit_string, + .commit_string = &commit_string, + .delete_surrounding_text = &delete_surrounding_text, + .done = &done, +}; + +#else /* !FOOT_IME_ENABLED */ + +void ime_enable(struct seat *seat) {} +void ime_disable(struct seat *seat) {} +void ime_update_cursor_rect(struct seat *seat) {} + +void ime_reset_pending_preedit(struct seat *seat) {} +void ime_reset_pending_commit(struct seat *seat) {} +void ime_reset_pending(struct seat *seat) {} +void ime_reset_preedit(struct seat *seat) {} + +#endif diff --git a/ime.h b/ime.h new file mode 100644 index 0000000..3127f4d --- /dev/null +++ b/ime.h @@ -0,0 +1,19 @@ +#pragma once + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + +#include "text-input-unstable-v3.h" + +extern const struct zwp_text_input_v3_listener text_input_listener; + +#endif /* FOOT_IME_ENABLED */ + +struct seat; +struct terminal; + +void ime_enable(struct seat *seat); +void ime_disable(struct seat *seat); +void ime_update_cursor_rect(struct seat *seat); + +void ime_reset_pending(struct seat *seat); +void ime_reset_preedit(struct seat *seat); diff --git a/input.c b/input.c new file mode 100644 index 0000000..0e5c031 --- /dev/null +++ b/input.c @@ -0,0 +1,3871 @@ +#include "input.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include + +#define LOG_MODULE "input" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "commands.h" +#include "config.h" +#include "grid.h" +#include "keymap.h" +#include "kitty-keymap.h" +#include "macros.h" +#include "quirks.h" +#include "render.h" +#include "search.h" +#include "selection.h" +#include "spawn.h" +#include "terminal.h" +#include "tokenize.h" +#include "unicode-mode.h" +#include "url-mode.h" +#include "util.h" +#include "vt.h" +#include "xkbcommon-vmod.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +struct pipe_context { + char *text; + size_t idx; + size_t left; +}; + +static bool +fdm_write_pipe(struct fdm *fdm, int fd, int events, void *data) +{ + struct pipe_context *ctx = data; + + if (events & EPOLLHUP) + goto pipe_closed; + + xassert(events & EPOLLOUT); + ssize_t written = write(fd, &ctx->text[ctx->idx], ctx->left); + + if (written < 0) { + LOG_WARN("failed to write to pipe: %s", strerror(errno)); + goto pipe_closed; + } + + xassert(written <= ctx->left); + ctx->idx += written; + ctx->left -= written; + + if (ctx->left == 0) + goto pipe_closed; + + return true; + +pipe_closed: + free(ctx->text); + free(ctx); + fdm_del(fdm, fd); + return true; +} + +static void alternate_scroll(struct seat *seat, int amount, int button); + +static bool +execute_binding(struct seat *seat, struct terminal *term, + const struct key_binding *binding, uint32_t serial, int amount) +{ + const enum bind_action_normal action = binding->action; + + switch (action) { + case BIND_ACTION_NONE: + return true; + + case BIND_ACTION_NOOP: + return true; + + case BIND_ACTION_SCROLLBACK_UP_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->rows); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_UP_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_UP_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, 1); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_UP_MOUSE: + if (term->grid == &term->alt) { + if (term->alt_scrolling) { + alternate_scroll(seat, amount, BTN_BACK); + return true; + } + } else { + cmd_scrollback_up(term, amount); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_DOWN_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->rows); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_DOWN_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_DOWN_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, 1); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_DOWN_MOUSE: + if (term->grid == &term->alt) { + if (term->alt_scrolling) { + alternate_scroll(seat, amount, BTN_FORWARD); + return true; + } + } else { + cmd_scrollback_down(term, amount); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_HOME: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->grid->num_rows); + return true; + } + break; + + case BIND_ACTION_SCROLLBACK_END: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->grid->num_rows); + return true; + } + break; + + case BIND_ACTION_CLIPBOARD_COPY: + selection_to_clipboard(seat, term, serial); + return true; + + case BIND_ACTION_CLIPBOARD_PASTE: + selection_from_clipboard(seat, term, serial); + term_reset_view(term); + return true; + + case BIND_ACTION_PRIMARY_PASTE: + selection_from_primary(seat, term); + term_reset_view(term); + return true; + + case BIND_ACTION_SEARCH_START: + search_begin(term); + return true; + + case BIND_ACTION_FONT_SIZE_UP: + term_font_size_increase(term); + return true; + + case BIND_ACTION_FONT_SIZE_DOWN: + term_font_size_decrease(term); + return true; + + case BIND_ACTION_FONT_SIZE_RESET: + term_font_size_reset(term); + return true; + + case BIND_ACTION_SPAWN_TERMINAL: + term_spawn_new(term); + return true; + + case BIND_ACTION_MINIMIZE: + xdg_toplevel_set_minimized(term->window->xdg_toplevel); + return true; + + case BIND_ACTION_MAXIMIZE: + if (term->window->is_fullscreen) + xdg_toplevel_unset_fullscreen(term->window->xdg_toplevel); + if (term->window->is_maximized) + xdg_toplevel_unset_maximized(term->window->xdg_toplevel); + else + xdg_toplevel_set_maximized(term->window->xdg_toplevel); + return true; + + case BIND_ACTION_FULLSCREEN: + if (term->window->is_fullscreen) + xdg_toplevel_unset_fullscreen(term->window->xdg_toplevel); + else + xdg_toplevel_set_fullscreen(term->window->xdg_toplevel, NULL); + return true; + + case BIND_ACTION_PIPE_SCROLLBACK: + if (term->grid == &term->alt) + break; + /* FALLTHROUGH */ + case BIND_ACTION_PIPE_VIEW: + case BIND_ACTION_PIPE_SELECTED: + case BIND_ACTION_PIPE_COMMAND_OUTPUT: { + if (binding->aux->type != BINDING_AUX_PIPE) + return true; + + struct pipe_context *ctx = NULL; + + int pipe_fd[2] = {-1, -1}; + int stdout_fd = -1; + int stderr_fd = -1; + + char *text = NULL; + size_t len = 0; + + if (pipe(pipe_fd) < 0) { + LOG_ERRNO("failed to create pipe"); + goto pipe_err; + } + + stdout_fd = open("/dev/null", O_WRONLY); + stderr_fd = open("/dev/null", O_WRONLY); + + if (stdout_fd < 0 || stderr_fd < 0) { + LOG_ERRNO("failed to open /dev/null"); + goto pipe_err; + } + + bool success; + switch (action) { + case BIND_ACTION_PIPE_SCROLLBACK: + success = term_scrollback_to_text(term, &text, &len); + break; + + case BIND_ACTION_PIPE_VIEW: + success = term_view_to_text(term, &text, &len); + break; + + case BIND_ACTION_PIPE_SELECTED: + text = selection_to_text(term); + success = text != NULL; + len = text != NULL ? strlen(text) : 0; + break; + + case BIND_ACTION_PIPE_COMMAND_OUTPUT: + success = term_command_output_to_text(term, &text, &len); + break; + + default: + BUG("Unhandled action type"); + success = false; + break; + } + + if (!success) + goto pipe_err; + + /* Make write-end non-blocking; required by the FDM */ + { + int flags = fcntl(pipe_fd[1], F_GETFL); + if (flags < 0 || + fcntl(pipe_fd[1], F_SETFL, flags | O_NONBLOCK) < 0) + { + LOG_ERRNO("failed to make write-end of pipe non-blocking"); + goto pipe_err; + } + } + + /* Make sure write-end is closed on exec() - or the spawned + * program may not terminate*/ + { + int flags = fcntl(pipe_fd[1], F_GETFD); + if (flags < 0 || + fcntl(pipe_fd[1], F_SETFD, flags | FD_CLOEXEC) < 0) + { + LOG_ERRNO("failed to set FD_CLOEXEC on writeend of pipe"); + goto pipe_err; + } + } + + if (spawn(term->reaper, term->cwd, binding->aux->pipe.args, + pipe_fd[0], stdout_fd, stderr_fd, NULL, NULL, NULL) < 0) + goto pipe_err; + + /* Close read end */ + close(pipe_fd[0]); + + ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct pipe_context){ + .text = text, + .left = len, + }; + + /* Asynchronously write the output to the pipe */ + if (!fdm_add(term->fdm, pipe_fd[1], EPOLLOUT, &fdm_write_pipe, ctx)) + goto pipe_err; + + return true; + + pipe_err: + if (stdout_fd >= 0) + close(stdout_fd); + if (stderr_fd >= 0) + close(stderr_fd); + if (pipe_fd[0] >= 0) + close(pipe_fd[0]); + if (pipe_fd[1] >= 0) + close(pipe_fd[1]); + free(text); + free(ctx); + return true; + } + + case BIND_ACTION_SHOW_URLS_COPY: + case BIND_ACTION_SHOW_URLS_LAUNCH: + case BIND_ACTION_SHOW_URLS_PERSISTENT: { + xassert(!urls_mode_is_active(term)); + + enum url_action url_action = + action == BIND_ACTION_SHOW_URLS_COPY ? URL_ACTION_COPY : + action == BIND_ACTION_SHOW_URLS_LAUNCH ? URL_ACTION_LAUNCH : + URL_ACTION_PERSISTENT; + + urls_collect(term, url_action, &term->conf->url.preg, true, &term->urls); + urls_assign_key_combos(term->conf, &term->urls); + urls_render(term, &term->conf->url.launch); + return true; + } + + case BIND_ACTION_TEXT_BINDING: + xassert(binding->aux->type == BINDING_AUX_TEXT); + term_to_slave(term, binding->aux->text.data, binding->aux->text.len); + return true; + + case BIND_ACTION_PROMPT_PREV: { + if (term->grid != &term->normal) + return false; + + struct grid *grid = term->grid; + const int sb_start = + grid_sb_start_ignore_uninitialized(grid, term->rows); + + /* Check each row from current view-1 (that is, the first + * currently not visible row), up to, and including, the + * scrollback start */ + for (int r_sb_rel = + grid_row_abs_to_sb_precalc_sb_start( + grid, sb_start, grid->view) - 1; + r_sb_rel >= 0; r_sb_rel--) + { + const int r_abs = + grid_row_sb_to_abs_precalc_sb_start(grid, sb_start, r_sb_rel); + + const struct row *row = grid->rows[r_abs]; + xassert(row != NULL); + + if (!row->shell_integration.prompt_marker) + continue; + + grid->view = r_abs; + term_damage_view(term); + render_refresh(term); + break; + } + + return true; + } + + case BIND_ACTION_PROMPT_NEXT: { + if (term->grid != &term->normal) + return false; + + struct grid *grid = term->grid; + const int num_rows = grid->num_rows; + + if (grid->view == grid->offset) { + /* Already at the bottom */ + return true; + } + + /* Check each row from view+1, to the bottom of the scrollback */ + for (int r_abs = (grid->view + 1) & (num_rows - 1); + ; + r_abs = (r_abs + 1) & (num_rows - 1)) + { + const struct row *row = grid->rows[r_abs]; + xassert(row != NULL); + + if (!row->shell_integration.prompt_marker) { + if (r_abs == grid->offset + term->rows - 1) { + /* We've reached the bottom of the scrollback */ + break; + } + continue; + } + + int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); + int ofs_sb_rel = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, grid->offset); + int new_view_sb_rel = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, r_abs); + + new_view_sb_rel = min(ofs_sb_rel, new_view_sb_rel); + grid->view = grid_row_sb_to_abs_precalc_sb_start( + grid, sb_start, new_view_sb_rel); + + term_damage_view(term); + render_refresh(term); + break; + } + + return true; + } + + case BIND_ACTION_UNICODE_INPUT: + unicode_mode_activate(term); + return true; + + case BIND_ACTION_QUIT: + term_shutdown(term); + return true; + + case BIND_ACTION_TAB_NEW: + term_tab_new(term, 0, NULL, NULL, term->shutdown.cb, term->shutdown.cb_data); + return true; + + case BIND_ACTION_TAB_CLOSE: + term_tab_close(term); + return true; + + case BIND_ACTION_TAB_NEXT: { + struct wl_window *win = term->window; + if (win->tab_count > 1) + term_tab_switch(win, (win->active_tab + 1) % win->tab_count); + return true; + } + + case BIND_ACTION_TAB_PREV: { + struct wl_window *win = term->window; + if (win->tab_count > 1) + term_tab_switch(win, + win->active_tab == 0 ? win->tab_count - 1 : win->active_tab - 1); + return true; + } + + case BIND_ACTION_TAB_1: + case BIND_ACTION_TAB_2: + case BIND_ACTION_TAB_3: + case BIND_ACTION_TAB_4: + case BIND_ACTION_TAB_5: + case BIND_ACTION_TAB_6: + case BIND_ACTION_TAB_7: + case BIND_ACTION_TAB_8: + case BIND_ACTION_TAB_9: { + size_t idx = (size_t)(action - BIND_ACTION_TAB_1); + struct wl_window *win = term->window; + if (idx < win->tab_count) + term_tab_switch(win, idx); + return true; + } + + case BIND_ACTION_TAB_OVERVIEW: + tab_overview_toggle(term->window); + return true; + + case BIND_ACTION_REGEX_LAUNCH: + case BIND_ACTION_REGEX_COPY: + if (binding->aux->type != BINDING_AUX_REGEX) + return true; + + tll_foreach(term->conf->custom_regexes, it) { + const struct custom_regex *regex = &it->item; + + if (streq(regex->name, binding->aux->regex_name)) { + xassert(!urls_mode_is_active(term)); + + enum url_action url_action = action == BIND_ACTION_REGEX_LAUNCH + ? URL_ACTION_LAUNCH : URL_ACTION_COPY; + + if (regex->regex == NULL) { + LOG_ERR("regex:%s has no regex defined", regex->name); + return true; + } + if (url_action == URL_ACTION_LAUNCH && regex->launch.argv.args == NULL) { + LOG_ERR("regex:%s has no launch command defined", regex->name); + return true; + } + + urls_collect(term, url_action, ®ex->preg, false, &term->urls); + urls_assign_key_combos(term->conf, &term->urls); + urls_render(term, ®ex->launch); + return true; + } + } + + LOG_ERR( + "no regex section named '%s' defined in the configuration", + binding->aux->regex_name); + + return true; + + case BIND_ACTION_THEME_SWITCH_1: + case BIND_ACTION_THEME_SWITCH_DARK: + term_theme_switch_to_dark(term); + return true; + + case BIND_ACTION_THEME_SWITCH_2: + case BIND_ACTION_THEME_SWITCH_LIGHT: + term_theme_switch_to_light(term); + return true; + + case BIND_ACTION_THEME_TOGGLE: + term_theme_toggle(term); + return true; + + case BIND_ACTION_SELECT_BEGIN: + selection_start( + term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, false); + return true; + + case BIND_ACTION_SELECT_BEGIN_BLOCK: + selection_start( + term, seat->mouse.col, seat->mouse.row, SELECTION_BLOCK, false); + return true; + + case BIND_ACTION_SELECT_EXTEND: + selection_extend( + seat, term, seat->mouse.col, seat->mouse.row, term->selection.kind); + return true; + + case BIND_ACTION_SELECT_EXTEND_CHAR_WISE: + if (term->selection.kind != SELECTION_BLOCK) { + selection_extend( + seat, term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE); + return true; + } + return false; + + case BIND_ACTION_SELECT_WORD: + selection_start( + term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE, false); + return true; + + case BIND_ACTION_SELECT_WORD_WS: + selection_start( + term, seat->mouse.col, seat->mouse.row, SELECTION_WORD_WISE, true); + return true; + + case BIND_ACTION_SELECT_QUOTE: + selection_start( + term, seat->mouse.col, seat->mouse.row, SELECTION_QUOTE_WISE, false); + return true; + + case BIND_ACTION_SELECT_ROW: + selection_start( + term, seat->mouse.col, seat->mouse.row, SELECTION_LINE_WISE, false); + return true; + + case BIND_ACTION_COUNT: + BUG("Invalid action type"); + return false; + } + + return false; +} + +static void +keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, + uint32_t format, int32_t fd, uint32_t size) +{ + LOG_DBG("keyboard_keymap: keyboard=%p (format=%u, size=%u)", + (void *)wl_keyboard, format, size); + + struct seat *seat = data; + struct wayland *wayl = seat->wayl; + + /* + * Free old keymap state + */ + + if (seat->kbd.xkb_keymap != NULL) { + xkb_keymap_unref(seat->kbd.xkb_keymap); + seat->kbd.xkb_keymap = NULL; + } + if (seat->kbd.xkb_state != NULL) { + xkb_state_unref(seat->kbd.xkb_state); + seat->kbd.xkb_state = NULL; + } + + key_binding_unload_keymap(wayl->key_binding_manager, seat); + + /* Verify keymap is in a format we understand */ + switch ((enum wl_keyboard_keymap_format)format) { + case WL_KEYBOARD_KEYMAP_FORMAT_NO_KEYMAP: + goto err; + + case WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1: + break; + + default: + LOG_WARN("unrecognized keymap format: %u", format); + goto err; + } + + char *map_str = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0); + if (map_str == MAP_FAILED) { + LOG_ERRNO("failed to mmap keyboard keymap"); + goto err; + } + + while (map_str[size - 1] == '\0') + size--; + + if (seat->kbd.xkb != NULL) { + seat->kbd.xkb_keymap = xkb_keymap_new_from_buffer( + seat->kbd.xkb, map_str, size, XKB_KEYMAP_FORMAT_TEXT_V1, + XKB_KEYMAP_COMPILE_NO_FLAGS); + + } + + munmap(map_str, size); + + if (seat->kbd.xkb_keymap != NULL) { + seat->kbd.xkb_state = xkb_state_new(seat->kbd.xkb_keymap); + + seat->kbd.mod_shift = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat->kbd.mod_alt = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; + seat->kbd.mod_ctrl = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat->kbd.mod_super = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat->kbd.mod_caps = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat->kbd.mod_num = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat->kbd.legacy_significant = 0; + if (seat->kbd.mod_shift != XKB_MOD_INVALID) + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_shift; + if (seat->kbd.mod_alt != XKB_MOD_INVALID) + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_alt; + if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_ctrl; + if (seat->kbd.mod_super != XKB_MOD_INVALID) + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat->kbd.kitty_significant = seat->kbd.legacy_significant; + if (seat->kbd.mod_caps != XKB_MOD_INVALID) + seat->kbd.kitty_significant |= 1 << seat->kbd.mod_caps; + if (seat->kbd.mod_num != XKB_MOD_INVALID) + seat->kbd.kitty_significant |= 1 << seat->kbd.mod_num; + + + /* + * Create a mask of all "virtual" modifiers. Some compositors + * add these *in addition* to the "real" modifiers (Mod1, + * Mod2, etc). + * + * Since our modifier logic (both for internal shortcut + * processing, and e.g. the kitty keyboard protocol) makes + * very few assumptions on available modifiers, which keys map + * to which modifier etc, the presence of virtual modifiers + * causes various things to break. + * + * For example, if a foot shortcut is Mod1+b (i.e. Alt+b), it + * won't match if the compositor _also_ sets the Alt modifier + * (the corresponding shortcut in foot would be Alt+Mod1+b). + * + * See https://codeberg.org/dnkl/foot/issues/2009 + * + * Mutter (GNOME) is known to set the virtual modifiers in + * addtiion to the real modifiers. + * + * As far as I know, there's no compositor that _only_ sets + * virtual modifiers (don't think that's even legal...?) + */ + { + xkb_mod_index_t alt = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_ALT); + xkb_mod_index_t meta = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_META); + xkb_mod_index_t super = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_SUPER); + xkb_mod_index_t hyper = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_HYPER); + xkb_mod_index_t num_lock = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_NUM); + xkb_mod_index_t scroll_lock = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_SCROLL); + xkb_mod_index_t level_three = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_LEVEL3); + xkb_mod_index_t level_five = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_VMOD_NAME_LEVEL5); + + xkb_mod_index_t ignore = 0; + + if (alt != XKB_MOD_INVALID) ignore |= 1 << alt; + if (meta != XKB_MOD_INVALID) ignore |= 1 << meta; + if (super != XKB_MOD_INVALID) ignore |= 1 << super; + if (hyper != XKB_MOD_INVALID) ignore |= 1 << hyper; + if (num_lock != XKB_MOD_INVALID) ignore |= 1 << num_lock; + if (scroll_lock != XKB_MOD_INVALID) ignore |= 1 << scroll_lock; + if (level_three != XKB_MOD_INVALID) ignore |= 1 << level_three; + if (level_five != XKB_MOD_INVALID) ignore |= 1 << level_five; + + seat->kbd.virtual_modifiers = ignore; + } + + seat->kbd.key_arrow_up = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "UP"); + seat->kbd.key_arrow_down = xkb_keymap_key_by_name(seat->kbd.xkb_keymap, "DOWN"); + } + + key_binding_load_keymap(wayl->key_binding_manager, seat); + +err: + close(fd); +} + +static void +keyboard_enter(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, + struct wl_surface *surface, struct wl_array *keys) +{ + xassert(surface != NULL); + xassert(serial != 0); + + struct seat *seat = data; + struct wl_window *win = wl_surface_get_user_data(surface); + struct terminal *term = win->term; + + LOG_DBG("%s: keyboard_enter: keyboard=%p, serial=%u, surface=%p", + seat->name, (void *)wl_keyboard, serial, (void *)surface); + + term_kbd_focus_in(term); + seat->kbd_focus = term; + seat->kbd.serial = serial; +} + +static bool +start_repeater(struct seat *seat, uint32_t key) +{ + if (seat->kbd.repeat.dont_re_repeat) + return true; + + if (seat->kbd.repeat.rate == 0) + return true; + + struct itimerspec t = { + .it_value = {.tv_sec = 0, .tv_nsec = seat->kbd.repeat.delay * 1000000}, + .it_interval = {.tv_sec = 0, .tv_nsec = 1000000000 / seat->kbd.repeat.rate}, + }; + + if (t.it_value.tv_nsec >= 1000000000) { + t.it_value.tv_sec += t.it_value.tv_nsec / 1000000000; + t.it_value.tv_nsec %= 1000000000; + } + if (t.it_interval.tv_nsec >= 1000000000) { + t.it_interval.tv_sec += t.it_interval.tv_nsec / 1000000000; + t.it_interval.tv_nsec %= 1000000000; + } + if (timerfd_settime(seat->kbd.repeat.fd, 0, &t, NULL) < 0) { + LOG_ERRNO("%s: failed to arm keyboard repeat timer", seat->name); + return false; + } + + seat->kbd.repeat.key = key; + return true; +} + +static bool +stop_repeater(struct seat *seat, uint32_t key) +{ + if (key != -1 && key != seat->kbd.repeat.key) + return true; + + if (timerfd_settime(seat->kbd.repeat.fd, 0, &(struct itimerspec){{0}}, NULL) < 0) { + LOG_ERRNO("%s: failed to disarm keyboard repeat timer", seat->name); + return false; + } + + return true; +} + +static void +keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, + struct wl_surface *surface) +{ + struct seat *seat = data; + + LOG_DBG("keyboard_leave: keyboard=%p, serial=%u, surface=%p", + (void *)wl_keyboard, serial, (void *)surface); + + xassert( + seat->kbd_focus == NULL || + surface == NULL || /* Seen on Sway 1.2 */ + ((const struct wl_window *)wl_surface_get_user_data(surface))->term == seat->kbd_focus + ); + + struct terminal *old_focused = seat->kbd_focus; + seat->kbd_focus = NULL; + + stop_repeater(seat, -1); + seat->kbd.shift = false; + seat->kbd.alt = false; + seat->kbd.ctrl = false; + seat->kbd.super = false; + + if (seat->kbd.xkb_compose_state != NULL) + xkb_compose_state_reset(seat->kbd.xkb_compose_state); + + if (seat->kbd.xkb_state != NULL && seat->kbd.xkb_keymap != NULL) { + const xkb_layout_index_t layout_count = xkb_keymap_num_layouts(seat->kbd.xkb_keymap); + + for (xkb_layout_index_t i = 0; i < layout_count; i++) + xkb_state_update_mask(seat->kbd.xkb_state, 0, 0, 0, i, i, i); + } + + if (old_focused != NULL) { + seat->pointer.hidden = false; + term_xcursor_update_for_seat(old_focused, seat); + term_kbd_focus_out(old_focused); + } else { + /* + * Sway bug - under certain conditions we get a + * keyboard_leave() (and keyboard_key()) without first having + * received a keyboard_enter() + */ + LOG_WARN( + "compositor sent keyboard_leave event without a keyboard_enter " + "event: surface=%p", (void *)surface); + } +} + +static const struct key_data * +keymap_data_for_sym(xkb_keysym_t sym, size_t *count) +{ + switch (sym) { + case XKB_KEY_Escape: *count = ALEN(key_escape); return key_escape; + case XKB_KEY_Return: *count = ALEN(key_return); return key_return; + case XKB_KEY_ISO_Left_Tab: *count = ALEN(key_iso_left_tab); return key_iso_left_tab; + case XKB_KEY_Tab: *count = ALEN(key_tab); return key_tab; + case XKB_KEY_BackSpace: *count = ALEN(key_backspace); return key_backspace; + case XKB_KEY_Up: *count = ALEN(key_up); return key_up; + case XKB_KEY_Down: *count = ALEN(key_down); return key_down; + case XKB_KEY_Right: *count = ALEN(key_right); return key_right; + case XKB_KEY_Left: *count = ALEN(key_left); return key_left; + case XKB_KEY_Home: *count = ALEN(key_home); return key_home; + case XKB_KEY_End: *count = ALEN(key_end); return key_end; + case XKB_KEY_Insert: *count = ALEN(key_insert); return key_insert; + case XKB_KEY_Delete: *count = ALEN(key_delete); return key_delete; + case XKB_KEY_Page_Up: *count = ALEN(key_pageup); return key_pageup; + case XKB_KEY_Page_Down: *count = ALEN(key_pagedown); return key_pagedown; + case XKB_KEY_F1: *count = ALEN(key_f1); return key_f1; + case XKB_KEY_F2: *count = ALEN(key_f2); return key_f2; + case XKB_KEY_F3: *count = ALEN(key_f3); return key_f3; + case XKB_KEY_F4: *count = ALEN(key_f4); return key_f4; + case XKB_KEY_F5: *count = ALEN(key_f5); return key_f5; + case XKB_KEY_F6: *count = ALEN(key_f6); return key_f6; + case XKB_KEY_F7: *count = ALEN(key_f7); return key_f7; + case XKB_KEY_F8: *count = ALEN(key_f8); return key_f8; + case XKB_KEY_F9: *count = ALEN(key_f9); return key_f9; + case XKB_KEY_F10: *count = ALEN(key_f10); return key_f10; + case XKB_KEY_F11: *count = ALEN(key_f11); return key_f11; + case XKB_KEY_F12: *count = ALEN(key_f12); return key_f12; + case XKB_KEY_F13: *count = ALEN(key_f13); return key_f13; + case XKB_KEY_F14: *count = ALEN(key_f14); return key_f14; + case XKB_KEY_F15: *count = ALEN(key_f15); return key_f15; + case XKB_KEY_F16: *count = ALEN(key_f16); return key_f16; + case XKB_KEY_F17: *count = ALEN(key_f17); return key_f17; + case XKB_KEY_F18: *count = ALEN(key_f18); return key_f18; + case XKB_KEY_F19: *count = ALEN(key_f19); return key_f19; + case XKB_KEY_F20: *count = ALEN(key_f20); return key_f20; + case XKB_KEY_F21: *count = ALEN(key_f21); return key_f21; + case XKB_KEY_F22: *count = ALEN(key_f22); return key_f22; + case XKB_KEY_F23: *count = ALEN(key_f23); return key_f23; + case XKB_KEY_F24: *count = ALEN(key_f24); return key_f24; + case XKB_KEY_F25: *count = ALEN(key_f25); return key_f25; + case XKB_KEY_F26: *count = ALEN(key_f26); return key_f26; + case XKB_KEY_F27: *count = ALEN(key_f27); return key_f27; + case XKB_KEY_F28: *count = ALEN(key_f28); return key_f28; + case XKB_KEY_F29: *count = ALEN(key_f29); return key_f29; + case XKB_KEY_F30: *count = ALEN(key_f30); return key_f30; + case XKB_KEY_F31: *count = ALEN(key_f31); return key_f31; + case XKB_KEY_F32: *count = ALEN(key_f32); return key_f32; + case XKB_KEY_F33: *count = ALEN(key_f33); return key_f33; + case XKB_KEY_F34: *count = ALEN(key_f34); return key_f34; + case XKB_KEY_F35: *count = ALEN(key_f35); return key_f35; + case XKB_KEY_KP_Up: *count = ALEN(key_kp_up); return key_kp_up; + case XKB_KEY_KP_Down: *count = ALEN(key_kp_down); return key_kp_down; + case XKB_KEY_KP_Right: *count = ALEN(key_kp_right); return key_kp_right; + case XKB_KEY_KP_Left: *count = ALEN(key_kp_left); return key_kp_left; + case XKB_KEY_KP_Begin: *count = ALEN(key_kp_begin); return key_kp_begin; + case XKB_KEY_KP_Home: *count = ALEN(key_kp_home); return key_kp_home; + case XKB_KEY_KP_End: *count = ALEN(key_kp_end); return key_kp_end; + case XKB_KEY_KP_Insert: *count = ALEN(key_kp_insert); return key_kp_insert; + case XKB_KEY_KP_Delete: *count = ALEN(key_kp_delete); return key_kp_delete; + case XKB_KEY_KP_Page_Up: *count = ALEN(key_kp_pageup); return key_kp_pageup; + case XKB_KEY_KP_Page_Down: *count = ALEN(key_kp_pagedown); return key_kp_pagedown; + case XKB_KEY_KP_Enter: *count = ALEN(key_kp_enter); return key_kp_enter; + case XKB_KEY_KP_Divide: *count = ALEN(key_kp_divide); return key_kp_divide; + case XKB_KEY_KP_Multiply: *count = ALEN(key_kp_multiply); return key_kp_multiply; + case XKB_KEY_KP_Subtract: *count = ALEN(key_kp_subtract); return key_kp_subtract; + case XKB_KEY_KP_Add: *count = ALEN(key_kp_add); return key_kp_add; + case XKB_KEY_KP_Separator: *count = ALEN(key_kp_separator); return key_kp_separator; + case XKB_KEY_KP_Decimal: *count = ALEN(key_kp_decimal); return key_kp_decimal; + case XKB_KEY_KP_0: *count = ALEN(key_kp_0); return key_kp_0; + case XKB_KEY_KP_1: *count = ALEN(key_kp_1); return key_kp_1; + case XKB_KEY_KP_2: *count = ALEN(key_kp_2); return key_kp_2; + case XKB_KEY_KP_3: *count = ALEN(key_kp_3); return key_kp_3; + case XKB_KEY_KP_4: *count = ALEN(key_kp_4); return key_kp_4; + case XKB_KEY_KP_5: *count = ALEN(key_kp_5); return key_kp_5; + case XKB_KEY_KP_6: *count = ALEN(key_kp_6); return key_kp_6; + case XKB_KEY_KP_7: *count = ALEN(key_kp_7); return key_kp_7; + case XKB_KEY_KP_8: *count = ALEN(key_kp_8); return key_kp_8; + case XKB_KEY_KP_9: *count = ALEN(key_kp_9); return key_kp_9; + } + + return NULL; +} + +static const struct key_data * +keymap_lookup(struct terminal *term, xkb_keysym_t sym, enum modifier mods) +{ + size_t count; + const struct key_data *info = keymap_data_for_sym(sym, &count); + + if (info == NULL) + return NULL; + + const enum cursor_keys cursor_keys_mode = term->cursor_keys_mode; + const enum keypad_keys keypad_keys_mode + = term->num_lock_modifier ? KEYPAD_NUMERICAL : term->keypad_keys_mode; + + LOG_DBG("keypad mode: %d", keypad_keys_mode); + + for (size_t j = 0; j < count; j++) { + enum modifier modifiers = info[j].modifiers; + + if (modifiers & MOD_MODIFY_OTHER_KEYS_STATE1) { + if (term->modify_other_keys_2) + continue; + modifiers &= ~MOD_MODIFY_OTHER_KEYS_STATE1; + } + if (modifiers & MOD_MODIFY_OTHER_KEYS_STATE2) { + if (!term->modify_other_keys_2) + continue; + modifiers &= ~MOD_MODIFY_OTHER_KEYS_STATE2; + } + + if (modifiers != MOD_ANY && modifiers != mods) + continue; + + if (info[j].cursor_keys_mode != CURSOR_KEYS_DONTCARE && + info[j].cursor_keys_mode != cursor_keys_mode) + continue; + + if (info[j].keypad_keys_mode != KEYPAD_DONTCARE && + info[j].keypad_keys_mode != keypad_keys_mode) + continue; + + return &info[j]; + } + + return NULL; +} + +UNITTEST +{ + struct terminal term = { + .num_lock_modifier = false, + .keypad_keys_mode = KEYPAD_NUMERICAL, + .cursor_keys_mode = CURSOR_KEYS_NORMAL, + }; + + const struct key_data *info = keymap_lookup(&term, XKB_KEY_ISO_Left_Tab, MOD_SHIFT | MOD_CTRL); + xassert(info != NULL); + xassert(streq(info->seq, "\033[27;6;9~")); +} + +UNITTEST +{ + struct terminal term = { + .modify_other_keys_2 = false, + }; + + const struct key_data *info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT); + xassert(info != NULL); + xassert(streq(info->seq, "\033\r")); + + term.modify_other_keys_2 = true; + info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT); + xassert(info != NULL); + xassert(streq(info->seq, "\033[27;3;13~")); +} + +void +get_current_modifiers(const struct seat *seat, + xkb_mod_mask_t *effective, + xkb_mod_mask_t *consumed, uint32_t key, + bool filter_locked) +{ + if (unlikely(seat->kbd.xkb_state == NULL)) { + if (effective != NULL) + *effective = 0; + if (consumed != NULL) + *consumed = 0; + } + + else { + const xkb_mod_mask_t locked = + xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + + if (effective != NULL) { + *effective = xkb_state_serialize_mods( + seat->kbd.xkb_state, XKB_STATE_MODS_EFFECTIVE); + + if (filter_locked) + *effective &= ~locked; + } + + if (consumed != NULL) { + *consumed = xkb_state_key_get_consumed_mods2( + seat->kbd.xkb_state, key, XKB_CONSUMED_MODE_XKB); + + if (filter_locked) + *consumed &= ~locked; + } + } +} + +struct kbd_ctx { + xkb_layout_index_t layout; + xkb_keycode_t key; + xkb_keysym_t sym; + + struct { + const xkb_keysym_t *syms; + size_t count; + } level0_syms; + + xkb_mod_mask_t mods; + xkb_mod_mask_t consumed; + + struct { + const uint8_t *buf; + size_t count; + } utf8; + uint32_t *utf32; + + enum xkb_compose_status compose_status; + enum wl_keyboard_key_state key_state; +}; + +static bool +legacy_kbd_protocol(struct seat *seat, struct terminal *term, + const struct kbd_ctx *ctx) +{ + if (ctx->key_state != WL_KEYBOARD_KEY_STATE_PRESSED) + return false; + if (ctx->compose_status == XKB_COMPOSE_COMPOSING) + return false; + + enum modifier keymap_mods = MOD_NONE; + keymap_mods |= seat->kbd.shift ? MOD_SHIFT : MOD_NONE; + keymap_mods |= seat->kbd.alt ? MOD_ALT : MOD_NONE; + keymap_mods |= seat->kbd.ctrl ? MOD_CTRL : MOD_NONE; + keymap_mods |= seat->kbd.super ? MOD_META : MOD_NONE; + + const xkb_keysym_t sym = ctx->sym; + const size_t count = ctx->utf8.count; + const uint8_t *const utf8 = ctx->utf8.buf; + + const struct key_data *keymap = keymap_lookup(term, sym, keymap_mods); + if (keymap != NULL) { + term_to_slave(term, keymap->seq, strlen(keymap->seq)); + return true; + } + + if (count == 0) + return false; + +#define is_control_key(x) ((x) >= 0x40 && (x) <= 0x7f) +#define IS_CTRL(x) ((x) < 0x20 || ((x) >= 0x7f && (x) <= 0x9f)) + + //LOG_DBG("term->modify_other_keys=%d, count=%zu, is_ctrl=%d (utf8=0x%02x), sym=%d", + //term->modify_other_keys_2, count, IS_CTRL(utf8[0]), utf8[0], sym); + + bool ctrl_is_in_effect = (keymap_mods & MOD_CTRL) != 0; + bool ctrl_seq = is_control_key(sym) || (count == 1 && IS_CTRL(utf8[0])); + + bool modify_other_keys2_in_effect = false; + + if (term->modify_other_keys_2) { + /* + * Try to mimic XTerm's behavior, when holding shift: + * + * - if other modifiers are pressed (e.g. Alt), emit a CSI escape + * - upper-case symbols A-Z are encoded as an CSI escape + * - other upper-case symbols (e.g 'Ö') or emitted as is + * - non-upper cased symbols are _mostly_ emitted as is (foot + * always emits as is) + * + * Examples (assuming Swedish layout): + * - Shift-a ('A') emits a CSI + * - Shift-, (';') emits ';' + * - Shift-Alt-, (Alt-;) emits a CSI + * - Shift-ö ('Ö') emits 'Ö' + */ + + /* Any modifiers, besides shift active? */ + const xkb_mod_mask_t shift_mask = 1 << seat->kbd.mod_shift; + if ((ctx->mods & ~shift_mask & seat->kbd.legacy_significant) != 0) + modify_other_keys2_in_effect = true; + + else { + const xkb_layout_index_t layout_idx = xkb_state_key_get_layout( + seat->kbd.xkb_state, ctx->key); + + /* + * Get pressed key's base symbol. + * - for 'A' (shift-a), that's 'a' + * - for ';' (shift-,), that's ',' + */ + const xkb_keysym_t *base_syms = NULL; + size_t base_count = xkb_keymap_key_get_syms_by_level( + seat->kbd.xkb_keymap, ctx->key, layout_idx, 0, &base_syms); + + /* Check if base symbol(s) is a-z. If so, emit CSI */ + const xkb_keysym_t lower_cased_sym = xkb_keysym_to_lower(ctx->sym); + for (size_t i = 0; i < base_count; i++) { + const xkb_keysym_t s = base_syms[i]; + if (lower_cased_sym == s && s >= XKB_KEY_a && s <= XKB_KEY_z) { + modify_other_keys2_in_effect = true; + break; + } + } + } + } + + if (keymap_mods != MOD_NONE && (modify_other_keys2_in_effect || + (ctrl_is_in_effect && !ctrl_seq))) + { + static const int mod_param_map[32] = { + [MOD_SHIFT] = 2, + [MOD_ALT] = 3, + [MOD_SHIFT | MOD_ALT] = 4, + [MOD_CTRL] = 5, + [MOD_SHIFT | MOD_CTRL] = 6, + [MOD_ALT | MOD_CTRL] = 7, + [MOD_SHIFT | MOD_ALT | MOD_CTRL] = 8, + [MOD_META] = 9, + [MOD_META | MOD_SHIFT] = 10, + [MOD_META | MOD_ALT] = 11, + [MOD_META | MOD_SHIFT | MOD_ALT] = 12, + [MOD_META | MOD_CTRL] = 13, + [MOD_META | MOD_SHIFT | MOD_CTRL] = 14, + [MOD_META | MOD_ALT | MOD_CTRL] = 15, + [MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL] = 16, + }; + + xassert(keymap_mods < ALEN(mod_param_map)); + int modify_param = mod_param_map[keymap_mods]; + xassert(modify_param != 0); + + char reply[32]; + size_t n = xsnprintf(reply, sizeof(reply), "\x1b[27;%d;%d~", modify_param, sym); + term_to_slave(term, reply, n); + } + + else if (keymap_mods & MOD_ALT) { + /* + * When the alt modifier is pressed, we do one out of three things: + * + * 1. we prefix the output bytes with ESC + * 2. we set the 8:th bit in the output byte + * 3. we ignore the alt modifier + * + * #1 is configured with \E[?1036, and is on by default + * + * If #1 has been disabled, we use #2, *if* it's a single byte + * we're emitting. Since this is a UTF-8 terminal, we then + * UTF8-encode the 8-bit character. #2 is configured with + * \E[?1034, and is on by default. + * + * Lastly, if both #1 and #2 have been disabled, the alt + * modifier is ignored. + */ + if (term->meta.esc_prefix) { + term_to_slave(term, "\x1b", 1); + term_to_slave(term, utf8, count); + } + + else if (term->meta.eight_bit && count == 1) { + const char32_t wc = 0x80 | utf8[0]; + + char utf8_meta[MB_CUR_MAX]; + size_t chars = c32rtomb(utf8_meta, wc, &(mbstate_t){0}); + + if (chars != (size_t)-1) + term_to_slave(term, utf8_meta, chars); + else + term_to_slave(term, utf8, count); + } + + else { + /* Alt ignored */ + term_to_slave(term, utf8, count); + } + } else + term_to_slave(term, utf8, count); + + return true; +} + +UNITTEST +{ + /* Verify the kitty keymap is sorted */ + xkb_keysym_t last = 0; + for (size_t i = 0; i < ALEN(kitty_keymap); i++) { + const struct kitty_key_data *e = &kitty_keymap[i]; + xassert(e->sym > last); + last = e->sym; + } +} + +static int +kitty_search(const void *_key, const void *_e) +{ + const xkb_keysym_t *key = _key; + const struct kitty_key_data *e = _e; + return *key - e->sym; +} + +static bool +kitty_kbd_protocol(struct seat *seat, struct terminal *term, + const struct kbd_ctx *ctx) +{ + const bool repeating = seat->kbd.repeat.dont_re_repeat; + const bool pressed = ctx->key_state == WL_KEYBOARD_KEY_STATE_PRESSED && !repeating; + const bool released = ctx->key_state == WL_KEYBOARD_KEY_STATE_RELEASED; + const bool composing = ctx->compose_status == XKB_COMPOSE_COMPOSING; + const bool composed = ctx->compose_status == XKB_COMPOSE_COMPOSED; + + const enum kitty_kbd_flags flags = + term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx]; + + const bool disambiguate = flags & KITTY_KBD_DISAMBIGUATE; + const bool report_events = flags & KITTY_KBD_REPORT_EVENT; + const bool report_alternate = flags & KITTY_KBD_REPORT_ALTERNATE; + const bool report_all_as_escapes = flags & KITTY_KBD_REPORT_ALL; + + if (!report_events && released) + return false; + + /* TODO: should we even bother with this, or just say it's not supported? */ + if (!disambiguate && !report_all_as_escapes && pressed) + return legacy_kbd_protocol(seat, term, ctx); + + const xkb_keysym_t sym = ctx->sym; + const uint32_t *utf32 = ctx->utf32; + const uint8_t *const utf8 = ctx->utf8.buf; + const size_t count = ctx->utf8.count; + + /* Lookup sym in the pre-defined keysym table */ + const struct kitty_key_data *info = bsearch( + &sym, kitty_keymap, ALEN(kitty_keymap), sizeof(kitty_keymap[0]), + &kitty_search); + xassert(info == NULL || info->sym == sym); + + xkb_mod_mask_t mods = 0; + xkb_mod_mask_t locked = 0; + xkb_mod_mask_t consumed = ctx->consumed; + + if (info != NULL && info->is_modifier) { + /* + * Special-case modifier keys. + * + * Normally, the "current" XKB state reflects the state + * *before* the current key event. In other words, the + * modifiers for key events that affect the modifier state + * (e.g. one of the control keys, or shift keys etc) does + * *not* include the key itself. + * + * Put another way, if you press "control", the modifier set + * is empty in the key press event, but contains "ctrl" in the + * release event. + * + * The kitty protocol mandates the modifier list contain the + * key itself, in *both* the press and release event. + * + * We handle this by updating the XKB state to *include* the + * current key, retrieve the set of modifiers (including the + * set of consumed modifiers), and then revert the XKB update. + */ + xkb_state_update_key( + seat->kbd.xkb_state, ctx->key, pressed ? XKB_KEY_DOWN : XKB_KEY_UP); + + get_current_modifiers(seat, &mods, NULL, 0, false); + + locked = xkb_state_serialize_mods( + seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + consumed = xkb_state_key_get_consumed_mods2( + seat->kbd.xkb_state, ctx->key, XKB_CONSUMED_MODE_XKB); + +#if 0 + /* + * TODO: according to the XKB docs, state updates should + * always be in pairs: each press should be followed by a + * release. However, doing this just breaks the xkb state. + * + * *Not* pairing the above press/release with a corresponding + * release/press appears to do exactly what we want. + */ + xkb_state_update_key( + seat->kbd.xkb_state, ctx->key, pressed ? XKB_KEY_UP : XKB_KEY_DOWN); +#endif + } else { + /* Same as ctx->mods, but *without* filtering locked modifiers */ + get_current_modifiers(seat, &mods, NULL, 0, false); + locked = xkb_state_serialize_mods( + seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + } + + mods &= seat->kbd.kitty_significant; + consumed &= seat->kbd.kitty_significant; + + /* + * A note on locked modifiers; they *are* a part of the protocol, + * and *should* be included in the modifier set reported in the + * key event. + * + * However, *only* if the key would result in a CSIu *without* the + * locked modifier being enabled + * + * Translated: if *another* modifier is active, or if + * report-all-keys-as-escapes is enabled, then we include the + * locked modifier in the key event. + * + * But, if the key event would result in plain text output without + * the locked modifier, then we "ignore" the locked modifier and + * emit plain text anyway. + */ + + bool is_text = count > 0 && utf32 != NULL && (mods & ~locked & ~consumed) == 0; + for (size_t i = 0; utf32[i] != U'\0'; i++) { + if (!isc32print(utf32[i])) { + is_text = false; + break; + } + } + + const bool report_associated_text = + (flags & KITTY_KBD_REPORT_ASSOCIATED) && is_text && !released; + + if (composing) { + /* We never emit anything while composing, *except* modifiers + * (and only in report-all-keys-as-escape-codes mode) */ + if (info != NULL && info->is_modifier) + goto emit_escapes; + + return false; + } + + if (report_all_as_escapes) + goto emit_escapes; + + if ((mods & ~locked & ~consumed) == 0) { + switch (sym) { + case XKB_KEY_Return: + if (!released) + term_to_slave(term, "\r", 1); + return true; + + case XKB_KEY_BackSpace: + if (!released) + term_to_slave(term, "\x7f", 1); + return true; + + case XKB_KEY_Tab: + if (!released) + term_to_slave(term, "\t", 1); + return true; + } + } + + /* Plain-text without modifiers, or commposed text, is emitted as-is */ + if (is_text && !released) { + term_to_slave(term, utf8, count); + return true; + } + +emit_escapes: + ; + unsigned int encoded_mods = 0; + if (seat->kbd.mod_shift != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_shift) ? (1 << 0) : 0; + if (seat->kbd.mod_alt != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_alt) ? (1 << 1) : 0; + if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_ctrl) ? (1 << 2) : 0; + if (seat->kbd.mod_super != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_super) ? (1 << 3) : 0; + if (seat->kbd.mod_caps != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_caps) ? (1 << 6) : 0; + if (seat->kbd.mod_num != XKB_MOD_INVALID) + encoded_mods |= mods & (1 << seat->kbd.mod_num) ? (1 << 7) : 0; + encoded_mods++; + + /* + * Figure out the main, alternate and base key codes. + * + * The main key is the unshifted version of the generated symbol, + * the alternate key is the shifted version, and base is the + * (unshifted) key assuming the default layout. + * + * For example, the user presses shift+a, then: + * - unshifted = 'a' + * - shifted = 'A' + * - base = 'a' + * + * Base will in many cases be the same as the unshifted key, but + * may differ if the active keyboard layout is non-ASCII (examples + * would be russian, or alternative layouts like neo etc). + * + * The shifted key is what we get from XKB, i.e. the resulting key + * from all active modifiers, plus the pressed key. + */ + int unshifted = -1, shifted = -1, base = -1; + char final; + + if (info != NULL) { + /* Use code from lookup table (cursor keys, enter, tab etc)*/ + if (!info->is_modifier || report_all_as_escapes) { + shifted = info->key; + final = info->final; + } + } else { + /* Use keysym (typically its Unicode codepoint value) */ + + if (composed) + shifted = utf32[0]; /* TODO: what if there are multiple codepoints? */ + else + shifted = xkb_keysym_to_utf32(sym); + + final = 'u'; + } + + if (shifted <= 0) + return false; + + /* Base layout key. I.e the symbol the pressed key produces in + * the base/default layout (layout idx 0) */ + const xkb_keysym_t *base_syms; + int base_sym_count = xkb_keymap_key_get_syms_by_level( + seat->kbd.xkb_keymap, ctx->key, 0, 0, &base_syms); + + if (base_sym_count > 0) + base = xkb_keysym_to_utf32(base_syms[0]); + + /* + * If the keysym is shifted, use its unshifted codepoint + * instead. In other words, ctrl+a and ctrl+shift+a should both + * use the same value for 'key' (97 - i.a. 'a'). + * + * However, don't do this if a non-significant modifier was used + * to generate the symbol. This is needed since we cannot encode + * non-significant modifiers, and thus the "extra" modifier(s) + * would get lost. + * + * Example: + * + * the Swedish layout has '2', QUOTATION MARK ("double quote"), + * '@', and '²' on the same key. '2' is the base symbol. + * + * Shift+2 results in QUOTATION MARK + * AltGr+2 results in '@' + * AltGr+Shift+2 results in '²' + * + * The kitty kbd protocol can't encode AltGr. So, if we always + * used the base symbol ('2'), Alt+Shift+2 would result in the + * same escape sequence as AltGr+Alt+Shift+2. + * + * (yes, this matches what kitty does, as of 0.23.1) + */ + const bool use_level0_sym = + (ctx->mods & ~seat->kbd.kitty_significant) == 0 && ctx->level0_syms.count > 0; + + unshifted = use_level0_sym ? xkb_keysym_to_utf32(ctx->level0_syms.syms[0]) : 0; + + xassert(encoded_mods >= 1); + + char event[4]; + if (report_events /*&& !pressed*/) { + /* Note: this deviates slightly from Kitty, which omits the + * ":1" subparameter for key press events */ + event[0] = ':'; + event[1] = '0' + (pressed ? 1 : repeating ? 2 : 3); + event[2] = '\0'; + } else + event[0] = '\0'; + + char buf[128], *p = buf; + size_t left = sizeof(buf); + size_t bytes; + + const int key = unshifted > 0 && isc32print(unshifted) && !composed ? unshifted : shifted; + const int alternate = shifted; + + if (final == 'u' || final == '~') { + bytes = snprintf(p, left, "\x1b[%u", key); + p += bytes; left -= bytes; + + if (report_alternate) { + bool emit_alternate = alternate > 0 && alternate != key; + bool emit_base = base > 0 && base != key && base != alternate && isc32print(base); + + if (emit_alternate) { + bytes = snprintf(p, left, ":%u", alternate); + p += bytes; left -= bytes; + } + + if (emit_base) { + bytes = snprintf( + p, left, "%s:%u", !emit_alternate ? ":" : "", base); + p += bytes; left -= bytes; + } + } + + bool emit_mods = encoded_mods > 1 || event[0] != '\0'; + + if (emit_mods) { + bytes = snprintf(p, left, ";%u%s", encoded_mods, event); + p += bytes; left -= bytes; + } + + if (report_associated_text) { + bytes = snprintf(p, left, "%s;%u", !emit_mods ? ";" : "", utf32[0]); + p += bytes; left -= bytes; + + /* Additional text codepoints */ + if (utf32[0] != U'\0') { + for (size_t i = 1; utf32[i] != U'\0'; i++) { + bytes = snprintf(p, left, ":%u", utf32[i]); + p += bytes; left -= bytes; + } + } + } + + bytes = snprintf(p, left, "%c", final); + p += bytes; left -= bytes; + } else { + if (encoded_mods > 1 || event[0] != '\0') { + bytes = snprintf(p, left, "\x1b[1;%u%s%c", encoded_mods, event, final); + p += bytes; left -= bytes; + } else { + bytes = snprintf(p, left, "\x1b[%c", final); + p += bytes; left -= bytes; + } + } + + return term_to_slave(term, buf, sizeof(buf) - left); +} + +/* Copied from libxkbcommon (internal function) */ +static bool +keysym_is_modifier(xkb_keysym_t keysym) +{ + return + (keysym >= XKB_KEY_Shift_L && keysym <= XKB_KEY_Hyper_R) || + /* libX11 only goes up to XKB_KEY_ISO_Level5_Lock. */ + (keysym >= XKB_KEY_ISO_Lock && keysym <= XKB_KEY_ISO_Last_Group_Lock) || + keysym == XKB_KEY_Mode_switch || + keysym == XKB_KEY_Num_Lock; +} + +#if defined(_DEBUG) +static void +modifier_string(xkb_mod_mask_t mods, size_t sz, char mod_str[static sz], const struct seat *seat) +{ + if (sz == 0) + return; + + mod_str[0] = '\0'; + + for (size_t i = 0; i < sizeof(xkb_mod_mask_t) * 8; i++) { + if (!(mods & (1u << i))) + continue; + + strcat(mod_str, xkb_keymap_mod_get_name(seat->kbd.xkb_keymap, i)); + strcat(mod_str, "+"); + } + + if (mod_str[0] != '\0') { + /* Strip the last '+' */ + mod_str[strlen(mod_str) - 1] = '\0'; + } + + if (mod_str[0] == '\0') { + strcpy(mod_str, ""); + } +} +#endif + +static void +key_press_release(struct seat *seat, struct terminal *term, uint32_t serial, + uint32_t key, uint32_t state) +{ + xassert(serial != 0); + + seat->kbd.serial = serial; + if (seat->kbd.xkb == NULL || + seat->kbd.xkb_keymap == NULL || + seat->kbd.xkb_state == NULL) + { + return; + } + + const bool pressed = state == WL_KEYBOARD_KEY_STATE_PRESSED; + //const bool repeated = pressed && seat->kbd.repeat.dont_re_repeat; + const bool released = state == WL_KEYBOARD_KEY_STATE_RELEASED; + + if (released) + stop_repeater(seat, key); + + if (pressed) + seat->kbd.last_shortcut_sym = XKB_KEYSYM_MAX + 1; + + bool should_repeat = + pressed && xkb_keymap_key_repeats(seat->kbd.xkb_keymap, key); + + xkb_keysym_t sym = xkb_state_key_get_one_sym(seat->kbd.xkb_state, key); + + if (pressed && term->conf->mouse.hide_when_typing && !keysym_is_modifier(sym)) { + seat->pointer.hidden = true; + term_xcursor_update_for_seat(term, seat); + } + + enum xkb_compose_status compose_status = XKB_COMPOSE_NOTHING; + + if (seat->kbd.xkb_compose_state != NULL) { + if (pressed) + xkb_compose_state_feed(seat->kbd.xkb_compose_state, sym); + compose_status = xkb_compose_state_get_status( + seat->kbd.xkb_compose_state); + } + + const bool composed = compose_status == XKB_COMPOSE_COMPOSED; + + xkb_mod_mask_t mods, consumed; + get_current_modifiers(seat, &mods, &consumed, key, true); + + xkb_layout_index_t layout_idx = + xkb_state_key_get_layout(seat->kbd.xkb_state, key); + + const xkb_keysym_t *raw_syms = NULL; + size_t raw_count = xkb_keymap_key_get_syms_by_level( + seat->kbd.xkb_keymap, key, layout_idx, 0, &raw_syms); + + const struct key_binding_set *bindings = key_binding_for( + seat->wayl->key_binding_manager, term->conf, seat); + xassert(bindings != NULL); + + if (term->unicode_mode.active) { + if (pressed) + unicode_mode_input(seat, term, sym); + return; + } + + else if (term->is_searching) { + if (pressed) { + if (should_repeat) + start_repeater(seat, key); + + search_input( + seat, term, bindings, key, sym, mods, consumed, + raw_syms, raw_count, serial); + } + return; + } + + else if (urls_mode_is_active(term)) { + if (pressed) { + if (should_repeat) + start_repeater(seat, key); + + urls_input( + seat, term, bindings, key, sym, mods, consumed, + raw_syms, raw_count, serial); + } + return; + } + + /* Tab overview: intercept navigation + activation while open. The + * configured tab-overview binding still fires (handled below), so the + * user can toggle it back off with the same key. */ + if (term->window != NULL && + term->window->tab_overview_state.target_open && pressed) { + struct wl_window *win = term->window; + __typeof__(win->tab_overview_state) *st = &win->tab_overview_state; + const size_t n = win->tab_count; + + bool handled = false; + switch (sym) { + case XKB_KEY_Escape: + tab_overview_toggle(win); + handled = true; + break; + case XKB_KEY_Return: + case XKB_KEY_KP_Enter: + if (st->sel_idx < n && st->sel_idx != win->active_tab) + term_tab_switch(win, st->sel_idx); + tab_overview_close_instant(win); + handled = true; + break; + case XKB_KEY_Left: + if (n > 0) + st->sel_idx = (st->sel_idx == 0) ? n - 1 : st->sel_idx - 1; + render_refresh(term); + handled = true; + break; + case XKB_KEY_Right: + if (n > 0) + st->sel_idx = (st->sel_idx + 1) % n; + render_refresh(term); + handled = true; + break; + case XKB_KEY_Up: + case XKB_KEY_Down: { + /* Step by one row in the cached card layout */ + if (st->card_count >= 2) { + int cur_y = st->cards[st->sel_idx].y; + int cur_x = st->cards[st->sel_idx].x; + int best = -1; + int best_d = INT_MAX; + for (size_t i = 0; i < st->card_count; i++) { + if (i == st->sel_idx) continue; + int dy = st->cards[i].y - cur_y; + if (sym == XKB_KEY_Up && dy >= 0) continue; + if (sym == XKB_KEY_Down && dy <= 0) continue; + int dx = st->cards[i].x - cur_x; + int d = dx * dx + dy * dy; + if (d < best_d) { best_d = d; best = (int)i; } + } + if (best >= 0) st->sel_idx = (size_t)best; + } + render_refresh(term); + handled = true; + break; + } + default: + if (sym >= XKB_KEY_1 && sym <= XKB_KEY_9) { + size_t idx = (size_t)(sym - XKB_KEY_1); + if (idx < n) { + if (idx != win->active_tab) + term_tab_switch(win, idx); + tab_overview_close_instant(win); + } + handled = true; + } + break; + } + + if (handled) { + seat->kbd.last_shortcut_sym = sym; + /* Don't try repeat; navigation is one-shot */ + return; + } + + /* Fall through to binding match so the user's tab-overview key + * can still toggle off, but swallow everything else. */ + } + +#if defined(_DEBUG) && defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + char sym_name[100]; + xkb_keysym_get_name(sym, sym_name, sizeof(sym_name)); + + char active_mods_str[256] = {0}; + char consumed_mods_str[256] = {0}; + char locked_mods_str[256] = {0}; + + const xkb_mod_mask_t locked = + xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + + modifier_string(mods, sizeof(active_mods_str), active_mods_str, seat); + modifier_string(consumed, sizeof(consumed_mods_str), consumed_mods_str, seat); + modifier_string(locked, sizeof(locked_mods_str), locked_mods_str, seat); + + LOG_DBG("%s: %s (%u/0x%x), seat=%s, term=%p, serial=%u, " + "mods=%s (0x%08x), consumed=%s (0x%08x), locked=%s (0x%08x), " + "repeats=%d", + pressed ? "pressed" : "released", sym_name, sym, sym, + seat->name, (void *)term, serial, + active_mods_str, mods, consumed_mods_str, consumed, + locked_mods_str, locked, should_repeat); +#endif + + /* + * User configurable bindings + */ + if (pressed) { + /* Match untranslated symbols */ + tll_foreach(bindings->key, it) { + const struct key_binding *bind = &it->item; + + if (bind->mods != mods || bind->mods == 0) + continue; + + for (size_t i = 0; i < raw_count; i++) { + if (bind->k.sym == raw_syms[i] && + execute_binding(seat, term, bind, serial, 1)) + { + seat->kbd.last_shortcut_sym = sym; + goto maybe_repeat; + } + } + } + + /* Match translated symbol */ + tll_foreach(bindings->key, it) { + const struct key_binding *bind = &it->item; + + if (bind->k.sym == sym && + bind->mods == (mods & ~consumed) && + execute_binding(seat, term, bind, serial, 1)) + { + seat->kbd.last_shortcut_sym = sym; + goto maybe_repeat; + } + } + + /* Match raw key code */ + tll_foreach(bindings->key, it) { + const struct key_binding *bind = &it->item; + + if (bind->mods != mods || bind->mods == 0) + continue; + + tll_foreach(bind->k.key_codes, code) { + if (code->item == key && + execute_binding(seat, term, bind, serial, 1)) + { + seat->kbd.last_shortcut_sym = sym; + goto maybe_repeat; + } + } + } + } + + /* Overview is modal: don't pass un-bound keys through to the PTY */ + if (term->window != NULL && + (term->window->tab_overview_state.target_open || + term->window->tab_overview_state.visible)) + { + return; + } + + if (released && seat->kbd.last_shortcut_sym == sym) { + /* + * Don't process a release event, if it corresponds to a + * triggered shortcut. + * + * 1. If we consumed a key (press) event, we shouldn't emit an + * escape for its release event. + * 2. Ignoring the incorrectness of doing so; this also caused + * us to reset the viewport. + * + * Background: if the kitty keyboard protocol was enabled, + * then the viewport was instantly reset to the bottom, after + * scrolling up. + */ + //seat->kbd.last_shortcut_sym = XKB_KEYSYM_MAX + 1; + goto maybe_repeat; + } + + /* + * Keys generating escape sequences + */ + + + /* + * Compose, and maybe emit "normal" character + */ + + xassert(seat->kbd.xkb_compose_state != NULL || !composed); + + if (compose_status == XKB_COMPOSE_CANCELLED) + goto maybe_repeat; + + int count = composed + ? xkb_compose_state_get_utf8(seat->kbd.xkb_compose_state, NULL, 0) + : xkb_state_key_get_utf8(seat->kbd.xkb_state, key, NULL, 0); + + /* Buffer for translated key. Use a static buffer in most cases, + * and use a malloc:ed buffer when necessary */ + uint8_t buf[32]; + uint8_t *utf8 = count < sizeof(buf) ? buf : xmalloc(count + 1); + uint32_t *utf32 = NULL; + + if (composed) { + xkb_compose_state_get_utf8( + seat->kbd.xkb_compose_state, (char *)utf8, count + 1); + + if (count > 0) + utf32 = ambstoc32((const char *)utf8); + } else { + xkb_state_key_get_utf8( + seat->kbd.xkb_state, key, (char *)utf8, count + 1); + + utf32 = xcalloc(2, sizeof(utf32[0])); + utf32[0] = xkb_state_key_get_utf32(seat->kbd.xkb_state, key); + } + + struct kbd_ctx ctx = { + .layout = layout_idx, + .key = key, + .sym = sym, + .level0_syms = { + .syms = raw_syms, + .count = raw_count, + }, + .mods = mods, + .consumed = consumed, + .utf8 = { + .buf = utf8, + .count = count, + }, + .utf32 = utf32, + .compose_status = compose_status, + .key_state = state, + }; + + bool handled = term->grid->kitty_kbd.flags[term->grid->kitty_kbd.idx] != 0 + ? kitty_kbd_protocol(seat, term, &ctx) + : legacy_kbd_protocol(seat, term, &ctx); + + if (composed && released) + xkb_compose_state_reset(seat->kbd.xkb_compose_state); + + if (utf8 != buf) + free(utf8); + + if (handled && !keysym_is_modifier(sym)) { + term_reset_view(term); + selection_cancel(term); + } + + free(utf32); + +maybe_repeat: + clock_gettime( + term->wl->presentation_clock_id, &term->render.input_time); + + if (should_repeat) + start_repeater(seat, key); +} + +static void +keyboard_key(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, + uint32_t time, uint32_t key, uint32_t state) +{ + struct seat *seat = data; + key_press_release(seat, seat->kbd_focus, serial, key + 8, state); +} + +static void +keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, uint32_t serial, + uint32_t mods_depressed, uint32_t mods_latched, + uint32_t mods_locked, uint32_t group) +{ + struct seat *seat = data; + + mods_depressed &= ~seat->kbd.virtual_modifiers; + mods_latched &= ~seat->kbd.virtual_modifiers; + mods_locked &= ~seat->kbd.virtual_modifiers; + +#if defined(_DEBUG) + char depressed[256]; + char latched[256]; + char locked[256]; + + modifier_string(mods_depressed, sizeof(depressed), depressed, seat); + modifier_string(mods_latched, sizeof(latched), latched, seat); + modifier_string(mods_locked, sizeof(locked), locked, seat); + + LOG_DBG( + "modifiers: depressed=%s (0x%x), latched=%s (0x%x), locked=%s (0x%x), " + "group=%u", + depressed, mods_depressed, latched, mods_latched, locked, mods_locked, + group); +#endif + + if (seat->kbd.xkb_state != NULL) { + xkb_state_update_mask( + seat->kbd.xkb_state, mods_depressed, mods_latched, mods_locked, 0, 0, group); + + /* Update state of modifiers we're interested in for e.g mouse events */ + seat->kbd.shift = seat->kbd.mod_shift != XKB_MOD_INVALID + ? xkb_state_mod_index_is_active( + seat->kbd.xkb_state, seat->kbd.mod_shift, XKB_STATE_MODS_EFFECTIVE) + : false; + seat->kbd.alt = seat->kbd.mod_alt != XKB_MOD_INVALID + ? xkb_state_mod_index_is_active( + seat->kbd.xkb_state, seat->kbd.mod_alt, XKB_STATE_MODS_EFFECTIVE) + : false; + seat->kbd.ctrl = seat->kbd.mod_ctrl != XKB_MOD_INVALID + ? xkb_state_mod_index_is_active( + seat->kbd.xkb_state, seat->kbd.mod_ctrl, XKB_STATE_MODS_EFFECTIVE) + : false; + seat->kbd.super = seat->kbd.mod_super != XKB_MOD_INVALID + ? xkb_state_mod_index_is_active( + seat->kbd.xkb_state, seat->kbd.mod_super, XKB_STATE_MODS_EFFECTIVE) + : false; + } + + if (seat->kbd_focus && seat->kbd_focus->active_surface == TERM_SURF_GRID) + term_xcursor_update_for_seat(seat->kbd_focus, seat); +} + +UNITTEST +{ + int chan[2]; + xassert(pipe2(chan, O_CLOEXEC) == 0); + + xassert(chan[0] >= 0); + xassert(chan[1] >= 0); + + struct config conf = {0}; + struct grid grid = {0}; + + struct terminal term = { + .conf = &conf, + .grid = &grid, + .ptmx = chan[1], + .selection = { + .coords = { + .start = {-1, -1}, + .end = {-1, -1}, + }, + .auto_scroll = { + .fd = -1, + }, + }, + }; + + struct key_binding_manager *key_binding_manager = key_binding_manager_new(); + + struct wayland wayl = { + .key_binding_manager = key_binding_manager, + .terms = tll_init(), + }; + + struct seat seat = { + .wayl = &wayl, + .name = "unittest", + }; + + tll_push_back(wayl.terms, &term); + term.wl = &wayl; + + seat.kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + xassert(seat.kbd.xkb != NULL); + + grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE; + + /* Swedish keymap */ + { + seat.kbd.xkb_keymap = xkb_keymap_new_from_names( + seat.kbd.xkb, &(struct xkb_rule_names){.layout = "se"}, XKB_KEYMAP_COMPILE_NO_FLAGS); + if (seat.kbd.xkb_keymap == NULL) { + /* Skip test */ + goto no_keymap; + } + + seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); + xassert(seat.kbd.xkb_state != NULL); + + seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; + seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat.kbd.legacy_significant = 0; + if (seat.kbd.mod_shift != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; + if (seat.kbd.mod_alt != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; + if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; + if (seat.kbd.mod_super != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat.kbd.kitty_significant = seat.kbd.legacy_significant; + if (seat.kbd.mod_caps != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; + if (seat.kbd.mod_num != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; + + key_binding_new_for_seat(key_binding_manager, &seat); + key_binding_load_keymap(key_binding_manager, &seat); + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_ctrl; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_A + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key: 97 = 'a', alternate: 65 = 'A', base: N/A, mods: 6 = ctrl+shift */ + const char expected_ctrl_shift_a[] = "\033[97:65;6u"; + xassert(count == strlen(expected_ctrl_shift_a)); + xassert(streq(escape, expected_ctrl_shift_a)); + + key_press_release(&seat, &term, 1337, KEY_A + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key;. 50 = '2', alternate: 34 = '"', base: N/A, 4 = alt+shift */ + const char expected_alt_shift_2[] = "\033[50:34;4u"; + xassert(count == strlen(expected_alt_shift_2)); + xassert(streq(escape, expected_alt_shift_2)); + + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_index_t alt_gr = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, "Mod5"); + xassert(alt_gr != XKB_MOD_INVALID); + + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt | 1u << alt_gr; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 178 = '²', alternate: N/A, base: 50 = '2', 4 = alt+shift (AltGr not part of the protocol) */ + const char expected_altgr_alt_shift_2[] = "\033[178::50;4u"; + xassert(count == strlen(expected_altgr_alt_shift_2)); + xassert(streq(escape, expected_altgr_alt_shift_2)); + + key_press_release(&seat, &term, 1337, KEY_2 + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_alt; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 127 = , alternate: N/A, base: N/A, 3 = alt */ + const char expected_alt_backspace[] = "\033[127;3u"; + xassert(count == strlen(expected_alt_backspace)); + xassert(streq(escape, expected_alt_backspace)); + + key_press_release(&seat, &term, 1337, KEY_BACKSPACE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_ENTER + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 13 = , alternate: N/A, base: N/A, 5 = ctrl */ + const char expected_ctrl_enter[] = "\033[13;5u"; + xassert(count == strlen(expected_ctrl_enter)); + xassert(streq(escape, expected_ctrl_enter)); + + key_press_release(&seat, &term, 1337, KEY_ENTER + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_TAB + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key; 9 = , alternate: N/A, base: N/A, 5 = ctrl */ + const char expected_ctrl_tab[] = "\033[9;5u"; + xassert(count == strlen(expected_ctrl_tab)); + xassert(streq(escape, expected_ctrl_tab)); + + key_press_release(&seat, &term, 1337, KEY_TAB + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + { + xkb_mod_mask_t mods = 1u << seat.kbd.mod_ctrl | 1u << seat.kbd.mod_shift; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 0); + key_press_release(&seat, &term, 1337, KEY_LEFT + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + const char expected_ctrl_shift_left[] = "\033[1;6D"; + xassert(count == strlen(expected_ctrl_shift_left)); + xassert(streq(escape, expected_ctrl_shift_left)); + + key_press_release(&seat, &term, 1337, KEY_LEFT + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + key_binding_unload_keymap(key_binding_manager, &seat); + key_binding_remove_seat(key_binding_manager, &seat); + + xkb_state_unref(seat.kbd.xkb_state); + xkb_keymap_unref(seat.kbd.xkb_keymap); + + seat.kbd.xkb_state = NULL; + seat.kbd.xkb_keymap = NULL; + } + + /* de(neo) keymap */ + { + seat.kbd.xkb_keymap = xkb_keymap_new_from_names( + seat.kbd.xkb, &(struct xkb_rule_names){.layout = "us,de(neo)"}, + XKB_KEYMAP_COMPILE_NO_FLAGS); + + if (seat.kbd.xkb_keymap == NULL) { + /* Skip test */ + goto no_keymap; + } + + seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); + xassert(seat.kbd.xkb_state != NULL); + + seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; + seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat.kbd.legacy_significant = 0; + if (seat.kbd.mod_shift != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; + if (seat.kbd.mod_alt != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; + if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; + if (seat.kbd.mod_super != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat.kbd.kitty_significant = seat.kbd.legacy_significant; + if (seat.kbd.mod_caps != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; + if (seat.kbd.mod_num != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; + + key_binding_new_for_seat(key_binding_manager, &seat); + key_binding_load_keymap(key_binding_manager, &seat); + + { + /* + * In the de(neo) layout, the Y key generates 'k'. This + * means we should get a key+alternate that indicates 'k', + * but a base key that is 'y'. + */ + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift | 1u << seat.kbd.mod_alt; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1); + key_press_release(&seat, &term, 1337, KEY_Y + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key: 107 = 'k', alternate: 75 = 'K', base: 121 = 'y', mods: 4 = alt+shift */ + const char expected_alt_shift_y[] = "\033[107:75:121;4u"; + xassert(count == strlen(expected_alt_shift_y)); + xassert(streq(escape, expected_alt_shift_y)); + + key_press_release(&seat, &term, 1337, KEY_Y + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + } + + key_binding_unload_keymap(key_binding_manager, &seat); + key_binding_remove_seat(key_binding_manager, &seat); + + xkb_state_unref(seat.kbd.xkb_state); + xkb_keymap_unref(seat.kbd.xkb_keymap); + + seat.kbd.xkb_state = NULL; + seat.kbd.xkb_keymap = NULL; + } + + /* us(intl) keymap */ + { + seat.kbd.xkb_keymap = xkb_keymap_new_from_names( + seat.kbd.xkb, &(struct xkb_rule_names){.layout = "us", .variant = "intl"}, + XKB_KEYMAP_COMPILE_NO_FLAGS); + + if (seat.kbd.xkb_keymap == NULL) { + /* Skip test */ + goto no_keymap; + } + + seat.kbd.xkb_state = xkb_state_new(seat.kbd.xkb_keymap); + xassert(seat.kbd.xkb_state != NULL); + + seat.kbd.xkb_compose_table = xkb_compose_table_new_from_locale( + seat.kbd.xkb, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); + if (seat.kbd.xkb_compose_table == NULL) + goto no_keymap; + + seat.kbd.xkb_compose_state = xkb_compose_state_new( + seat.kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); + if (seat.kbd.xkb_compose_state == NULL) { + xkb_compose_table_unref(seat.kbd.xkb_compose_table); + goto no_keymap; + } + + seat.kbd.mod_shift = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_SHIFT); + seat.kbd.mod_alt = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_ALT) ; + seat.kbd.mod_ctrl = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CTRL); + seat.kbd.mod_super = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_LOGO); + seat.kbd.mod_caps = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_CAPS); + seat.kbd.mod_num = xkb_keymap_mod_get_index(seat.kbd.xkb_keymap, XKB_MOD_NAME_NUM); + + /* Significant modifiers in the legacy keyboard protocol */ + seat.kbd.legacy_significant = 0; + if (seat.kbd.mod_shift != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_shift; + if (seat.kbd.mod_alt != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_alt; + if (seat.kbd.mod_ctrl != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_ctrl; + if (seat.kbd.mod_super != XKB_MOD_INVALID) + seat.kbd.legacy_significant |= 1 << seat.kbd.mod_super; + + /* Significant modifiers in the kitty keyboard protocol */ + seat.kbd.kitty_significant = seat.kbd.legacy_significant; + if (seat.kbd.mod_caps != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_caps; + if (seat.kbd.mod_num != XKB_MOD_INVALID) + seat.kbd.kitty_significant |= 1 << seat.kbd.mod_num; + + key_binding_new_for_seat(key_binding_manager, &seat); + key_binding_load_keymap(key_binding_manager, &seat); + + { + /* + * Test the compose sequence "shift+', shift+space" + * + * Should result in a double quote, but a regression + * caused it to instead emit a space. See #1987 + * + * Note: "shift+', space" also results in a double quote, + * but never regressed to a space. + */ + grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE; + xkb_compose_state_reset(seat.kbd.xkb_compose_state); + + xkb_mod_mask_t mods = 1u << seat.kbd.mod_shift; + keyboard_modifiers(&seat, NULL, 1337, mods, 0, 0, 1); + + key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + key_press_release(&seat, &term, 1337, KEY_APOSTROPHE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + + key_press_release(&seat, &term, 1337, KEY_SPACE + 8, WL_KEYBOARD_KEY_STATE_PRESSED); + + char escape[64] = {0}; + ssize_t count = read(chan[0], escape, sizeof(escape)); + + /* key: 34 = '"', alternate: N/A, base: N/A, mods: 2 = shift */ + const char expected_shift_apostrophe[] = "\033[34;2u"; + xassert(count == strlen(expected_shift_apostrophe)); + xassert(streq(escape, expected_shift_apostrophe)); + + key_press_release(&seat, &term, 1337, KEY_SPACE + 8, WL_KEYBOARD_KEY_STATE_RELEASED); + + grid.kitty_kbd.flags[0] = KITTY_KBD_DISAMBIGUATE | KITTY_KBD_REPORT_ALTERNATE; + } + + key_binding_unload_keymap(key_binding_manager, &seat); + key_binding_remove_seat(key_binding_manager, &seat); + + xkb_compose_state_unref(seat.kbd.xkb_compose_state); + xkb_compose_table_unref(seat.kbd.xkb_compose_table); + + xkb_state_unref(seat.kbd.xkb_state); + xkb_keymap_unref(seat.kbd.xkb_keymap); + + seat.kbd.xkb_state = NULL; + seat.kbd.xkb_keymap = NULL; + } + +no_keymap: + xkb_context_unref(seat.kbd.xkb); + key_binding_manager_destroy(key_binding_manager); + + tll_free(wayl.terms); + close(chan[0]); + close(chan[1]); +} + +static void +keyboard_repeat_info(void *data, struct wl_keyboard *wl_keyboard, + int32_t rate, int32_t delay) +{ + struct seat *seat = data; + LOG_DBG("keyboard repeat: rate=%d, delay=%d", rate, delay); + seat->kbd.repeat.rate = rate; + seat->kbd.repeat.delay = delay; +} + +const struct wl_keyboard_listener keyboard_listener = { + .keymap = &keyboard_keymap, + .enter = &keyboard_enter, + .leave = &keyboard_leave, + .key = &keyboard_key, + .modifiers = &keyboard_modifiers, + .repeat_info = &keyboard_repeat_info, +}; + +void +input_repeat(struct seat *seat, uint32_t key) +{ + /* Should be cleared as soon as we loose focus */ + xassert(seat->kbd_focus != NULL); + struct terminal *term = seat->kbd_focus; + + key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_DOWN); +} + +static bool +is_top_left(const struct terminal *term, int x, int y) +{ + int csd_border_size = term->conf->csd.border_width; + return ( + (!term->window->is_constrained_top && !term->window->is_constrained_left) && + ((term->active_surface == TERM_SURF_BORDER_LEFT && y < 10 * term->scale) || + (term->active_surface == TERM_SURF_BORDER_TOP && x < (10 + csd_border_size) * term->scale))); +} + +static bool +is_top_right(const struct terminal *term, int x, int y) +{ + int csd_border_size = term->conf->csd.border_width; + return ( + (!term->window->is_constrained_top && !term->window->is_constrained_right) && + ((term->active_surface == TERM_SURF_BORDER_RIGHT && y < 10 * term->scale) || + (term->active_surface == TERM_SURF_BORDER_TOP && x > term->width + 1 * csd_border_size * term->scale - 10 * term->scale))); +} + +static bool +is_bottom_left(const struct terminal *term, int x, int y) +{ + int csd_title_size = term->conf->csd.title_height; + int csd_border_size = term->conf->csd.border_width; + return ( + (!term->window->is_constrained_bottom && !term->window->is_constrained_left) && + ((term->active_surface == TERM_SURF_BORDER_LEFT && y > csd_title_size * term->scale + term->height) || + (term->active_surface == TERM_SURF_BORDER_BOTTOM && x < (10 + csd_border_size) * term->scale))); +} + +static bool +is_bottom_right(const struct terminal *term, int x, int y) +{ + int csd_title_size = term->conf->csd.title_height; + int csd_border_size = term->conf->csd.border_width; + return ( + (!term->window->is_constrained_bottom && !term->window->is_constrained_right) && + ((term->active_surface == TERM_SURF_BORDER_RIGHT && y > csd_title_size * term->scale + term->height) || + (term->active_surface == TERM_SURF_BORDER_BOTTOM && x > term->width + 1 * csd_border_size * term->scale - 10 * term->scale))); +} + +enum cursor_shape +xcursor_for_csd_border(struct terminal *term, int x, int y) +{ + if (is_top_left(term, x, y)) return CURSOR_SHAPE_TOP_LEFT_CORNER; + else if (is_top_right(term, x, y)) return CURSOR_SHAPE_TOP_RIGHT_CORNER; + else if (is_bottom_left(term, x, y)) return CURSOR_SHAPE_BOTTOM_LEFT_CORNER; + else if (is_bottom_right(term, x, y)) return CURSOR_SHAPE_BOTTOM_RIGHT_CORNER; + + else if (term->active_surface == TERM_SURF_BORDER_LEFT) + return !term->window->is_constrained_left + ? CURSOR_SHAPE_LEFT_SIDE : CURSOR_SHAPE_LEFT_PTR; + + else if (term->active_surface == TERM_SURF_BORDER_RIGHT) + return !term->window->is_constrained_right + ? CURSOR_SHAPE_RIGHT_SIDE : CURSOR_SHAPE_LEFT_PTR; + + else if (term->active_surface == TERM_SURF_BORDER_TOP) + return !term->window->is_constrained_top + ? CURSOR_SHAPE_TOP_SIDE : CURSOR_SHAPE_LEFT_PTR; + + else if (term->active_surface == TERM_SURF_BORDER_BOTTOM) + return !term->window->is_constrained_bottom + ? CURSOR_SHAPE_BOTTOM_SIDE : CURSOR_SHAPE_LEFT_PTR; + + else { + BUG("Unreachable"); + return CURSOR_SHAPE_NONE; + } +} + +static void +mouse_button_state_reset(struct seat *seat) +{ + tll_free(seat->mouse.buttons); + seat->mouse.count = 0; + seat->mouse.last_released_button = 0; + memset(&seat->mouse.last_time, 0, sizeof(seat->mouse.last_time)); +} + +static void +mouse_coord_pixel_to_cell(struct seat *seat, const struct terminal *term, + int x, int y) +{ + /* + * Translate x,y pixel coordinate to a cell coordinate, or -1 + * if the cursor is outside the grid. I.e. if it is inside the + * margins. + */ + if (x < term->margins.left) + seat->mouse.col = 0; + else if (x >= term->width - term->margins.right) + seat->mouse.col = term->cols - 1; + else + seat->mouse.col = (x - term->margins.left) / term->cell_width; + + if (y < term->margins.top) + seat->mouse.row = 0; + else if (y >= term->height - term->margins.bottom) + seat->mouse.row = term->rows - 1; + else + seat->mouse.row = (y - term->margins.top) / term->cell_height; +} + +static bool +touch_is_active(const struct seat *seat) +{ + if (seat->wl_touch == NULL) { + return false; + } + + switch (seat->touch.state) { + case TOUCH_STATE_IDLE: + case TOUCH_STATE_INHIBITED: + return false; + + case TOUCH_STATE_HELD: + case TOUCH_STATE_DRAGGING: + case TOUCH_STATE_SCROLLING: + return true; + } + + BUG("Bad touch state: %d", seat->touch.state); + return false; +} + +static void +wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, struct wl_surface *surface, + wl_fixed_t surface_x, wl_fixed_t surface_y) +{ + if (unlikely(surface == NULL)) { + /* Seen on mutter-3.38 */ + LOG_WARN("compositor sent pointer_enter event with a NULL surface"); + return; + } + + struct seat *seat = data; + + struct wl_window *win = wl_surface_get_user_data(surface); + struct terminal *term = win->term; + + seat->mouse_focus = term; + term->active_surface = term_surface_kind(term, surface); + + if (touch_is_active(seat)) + return; + + int x = wl_fixed_to_int(surface_x) * term->scale; + int y = wl_fixed_to_int(surface_y) * term->scale; + + seat->pointer.serial = serial; + seat->pointer.hidden = false; + seat->mouse.x = x; + seat->mouse.y = y; + + LOG_DBG("pointer-enter: pointer=%p, serial=%u, surface = %p, new-moused = %p, " + "x=%d, y=%d", + (void *)wl_pointer, serial, (void *)surface, (void *)term, + x, y); + + xassert(tll_length(seat->mouse.buttons) == 0); + + wayl_reload_xcursor_theme(seat, term->scale); /* Scale may have changed */ + term_xcursor_update_for_seat(term, seat); + + switch (term->active_surface) { + case TERM_SURF_GRID: { + mouse_coord_pixel_to_cell(seat, term, x, y); + break; + } + + case TERM_SURF_TITLE: + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + case TERM_SURF_TAB_BAR: + case TERM_SURF_TAB_OVERVIEW: + break; + + case TERM_SURF_BUTTON_MINIMIZE: + case TERM_SURF_BUTTON_MAXIMIZE: + case TERM_SURF_BUTTON_CLOSE: + render_refresh_csd(term); + break; + + case TERM_SURF_NONE: + BUG("Invalid surface type"); + break; + } +} + +static void +wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, struct wl_surface *surface) +{ + struct seat *seat = data; + + if (seat->wl_touch != NULL) { + switch (seat->touch.state) { + case TOUCH_STATE_IDLE: + break; + + case TOUCH_STATE_INHIBITED: + seat->touch.state = TOUCH_STATE_IDLE; + break; + + case TOUCH_STATE_HELD: + case TOUCH_STATE_DRAGGING: + case TOUCH_STATE_SCROLLING: + return; + } + } + + struct terminal *old_moused = seat->mouse_focus; + + LOG_DBG( + "%s: pointer-leave: pointer=%p, serial=%u, surface = %p, old-moused = %p", + seat->name, (void *)wl_pointer, serial, (void *)surface, + (void *)old_moused); + + seat->pointer.hidden = false; + + if (seat->pointer.xcursor_callback != NULL) { + /* A cursor frame callback may never be called if the pointer leaves our surface */ + wl_callback_destroy(seat->pointer.xcursor_callback); + seat->pointer.xcursor_callback = NULL; + seat->pointer.xcursor_pending = false; + } + + /* Reset last-set-xcursor, to ensure we update it on a pointer-enter event */ + seat->pointer.shape = CURSOR_SHAPE_NONE; + + /* Reset mouse state */ + seat->mouse.x = seat->mouse.y = 0; + seat->mouse.col = seat->mouse.row = 0; + mouse_button_state_reset(seat); + for (size_t i = 0; i < ALEN(seat->mouse.aggregated); i++) + seat->mouse.aggregated[i] = 0.0; + seat->mouse.have_discrete = false; + + seat->mouse_focus = NULL; + if (old_moused == NULL) { + LOG_WARN( + "compositor sent pointer_leave event without a pointer_enter " + "event: surface=%p", (void *)surface); + } else { + if (surface != NULL) { + /* Sway 1.4 sends this event with a NULL surface when we destroy the window */ + const struct wl_window UNUSED *win = wl_surface_get_user_data(surface); + xassert(old_moused->window == win); + } + + enum term_surface active_surface = old_moused->active_surface; + + old_moused->active_surface = TERM_SURF_NONE; + + switch (active_surface) { + case TERM_SURF_BUTTON_MINIMIZE: + case TERM_SURF_BUTTON_MAXIMIZE: + case TERM_SURF_BUTTON_CLOSE: + if (old_moused->shutdown.in_progress) + break; + + render_refresh_csd(old_moused); + break; + + case TERM_SURF_GRID: + selection_finalize(seat, old_moused, seat->pointer.serial); + break; + + case TERM_SURF_NONE: + case TERM_SURF_TITLE: + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + case TERM_SURF_TAB_BAR: + case TERM_SURF_TAB_OVERVIEW: + break; + } + + } +} + +static bool +pointer_is_on_button(const struct terminal *term, const struct seat *seat, + enum csd_surface csd_surface) +{ + if (seat->mouse.x < 0) + return false; + if (seat->mouse.y < 0) + return false; + + struct csd_data info = get_csd_data(term, csd_surface); + if (seat->mouse.x > info.width) + return false; + + if (seat->mouse.y > info.height) + return false; + + return true; +} + +static void +wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, + uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) +{ + struct seat *seat = data; + + /* Touch-emulated pointer events have wl_pointer == NULL. */ + if (wl_pointer != NULL && touch_is_active(seat)) + return; + + struct wayland *wayl = seat->wayl; + struct terminal *term = seat->mouse_focus; + + if (unlikely(term == NULL)) { + /* Typically happens when the compositor sent a pointer enter + * event with a NULL surface - see wl_pointer_enter(). + * + * In this case, we never set seat->mouse_focus (since we + * can't map the enter event to a specific window). */ + return; + } + + struct wl_window *win = term->window; + + LOG_DBG("pointer_motion: pointer=%p, x=%d, y=%d", (void *)wl_pointer, + wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); + + xassert(term != NULL); + + int x = wl_fixed_to_int(surface_x) * term->scale; + int y = wl_fixed_to_int(surface_y) * term->scale; + + enum term_surface surf_kind = term->active_surface; + int button = 0; + bool send_to_client = false; + bool is_on_button = false; + + /* If current surface is a button, check if pointer was on it + *before* the motion event */ + switch (surf_kind) { + case TERM_SURF_BUTTON_MINIMIZE: + is_on_button = pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE); + break; + + case TERM_SURF_BUTTON_MAXIMIZE: + is_on_button = pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE); + break; + + case TERM_SURF_BUTTON_CLOSE: + is_on_button = pointer_is_on_button(term, seat, CSD_SURF_CLOSE); + break; + + case TERM_SURF_NONE: + case TERM_SURF_GRID: + case TERM_SURF_TITLE: + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + case TERM_SURF_TAB_BAR: + case TERM_SURF_TAB_OVERVIEW: + break; + } + + seat->pointer.hidden = false; + seat->mouse.x = x; + seat->mouse.y = y; + + term_xcursor_update_for_seat(term, seat); + + if (tll_length(seat->mouse.buttons) > 0) { + const struct button_tracker *tracker = &tll_front(seat->mouse.buttons); + surf_kind = tracker->surf_kind; + button = tracker->button; + send_to_client = tracker->send_to_client; + } + + switch (surf_kind) { + case TERM_SURF_NONE: + break; + + case TERM_SURF_BUTTON_MINIMIZE: + if (pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE) != is_on_button) + render_refresh_csd(term); + break; + + case TERM_SURF_BUTTON_MAXIMIZE: + if (pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE) != is_on_button) + render_refresh_csd(term); + break; + + case TERM_SURF_BUTTON_CLOSE: + if (pointer_is_on_button(term, seat, CSD_SURF_CLOSE) != is_on_button) + render_refresh_csd(term); + break; + + case TERM_SURF_TITLE: + /* We've started a 'move' timer, but user started dragging + * right away - abort the timer and initiate the actual move + * right away */ + if (button == BTN_LEFT && win->csd.move_timeout_fd != -1) { + fdm_del(wayl->fdm, win->csd.move_timeout_fd); + win->csd.move_timeout_fd = -1; + xdg_toplevel_move(win->xdg_toplevel, seat->wl_seat, win->csd.serial); + } + break; + + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + case TERM_SURF_TAB_BAR: + break; + + case TERM_SURF_TAB_OVERVIEW: { + const int new_hover = tab_overview_hit_test(win, x, y); + if (new_hover != win->tab_overview_state.hover_idx) { + win->tab_overview_state.hover_idx = new_hover; + render_refresh(term); + } + break; + } + + case TERM_SURF_GRID: { + int old_col = seat->mouse.col; + int old_row = seat->mouse.row; + + mouse_coord_pixel_to_cell(seat, term, seat->mouse.x, seat->mouse.y); + + xassert(seat->mouse.col >= 0 && seat->mouse.col < term->cols); + xassert(seat->mouse.row >= 0 && seat->mouse.row < term->rows); + + /* Cursor has moved to a different cell since last time */ + bool cursor_is_on_new_cell + = old_col != seat->mouse.col || old_row != seat->mouse.row; + + if (cursor_is_on_new_cell) { + /* Prevent multiple/different mouse bindings from + * triggering if the mouse has moved "too much" (to + * another cell) */ + seat->mouse.count = 0; + } + + /* Cursor is inside the grid, i.e. *not* in the margins */ + const bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0; + + enum selection_scroll_direction auto_scroll_direction + = term->selection.coords.end.row < 0 + ? SELECTION_SCROLL_NOT + : y < term->margins.top + ? SELECTION_SCROLL_UP + : y > term->height - term->margins.bottom + ? SELECTION_SCROLL_DOWN + : SELECTION_SCROLL_NOT; + + if (auto_scroll_direction == SELECTION_SCROLL_NOT) + selection_stop_scroll_timer(term); + + /* Update selection */ + if (!term->is_searching) { + if (auto_scroll_direction != SELECTION_SCROLL_NOT) { + /* + * Start 'selection auto-scrolling' + * + * The speed of the scrolling is proportional to the + * distance between the mouse and the grid; the + * further away the mouse is, the faster we scroll. + * + * Note that the speed is measured in 'intervals (in + * ns) between each timed scroll of a single line'. + * + * Thus, the further away the mouse is, the smaller + * interval value we use. + */ + + int distance = auto_scroll_direction == SELECTION_SCROLL_UP + ? term->margins.top - y + : y - (term->height - term->margins.bottom); + + xassert(distance > 0); + int divisor + = distance * term->conf->scrollback.multiplier / term->scale; + + selection_start_scroll_timer( + term, 400000000 / (divisor > 0 ? divisor : 1), + auto_scroll_direction, seat->mouse.col); + } + + if (term->selection.ongoing && + (cursor_is_on_new_cell || + (term->selection.coords.end.row < 0 && + seat->mouse.x >= term->margins.left && + seat->mouse.x < term->width - term->margins.right && + seat->mouse.y >= term->margins.top && + seat->mouse.y < term->height - term->margins.bottom))) + { + selection_update(term, seat->mouse.col, seat->mouse.row); + } + } + + /* Send mouse event to client application */ + if (!term_mouse_grabbed(term, seat) && + (cursor_is_on_new_cell || + term->mouse_reporting == MOUSE_SGR_PIXELS) && + ((button == 0 && cursor_is_on_grid) || + (button != 0 && send_to_client))) + { + xassert(seat->mouse.col < term->cols); + xassert(seat->mouse.row < term->rows); + + term_mouse_motion( + term, button, + seat->mouse.row, seat->mouse.col, + seat->mouse.y - term->margins.top, + seat->mouse.x - term->margins.left, + seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); + } + break; + } + } +} + +static bool +fdm_csd_move(struct fdm *fdm, int fd, int events, void *data) +{ + struct seat *seat = data; + fdm_del(fdm, fd); + + if (seat->mouse_focus == NULL) { + LOG_WARN( + "%s: CSD move timeout triggered, but seat's has no mouse focused terminal", + seat->name); + return true; + } + + struct wl_window *win = seat->mouse_focus->window; + + win->csd.move_timeout_fd = -1; + xdg_toplevel_move(win->xdg_toplevel, seat->wl_seat, win->csd.serial); + return true; +} + +static const struct key_binding * +match_mouse_binding(const struct seat *seat, const struct terminal *term, + int button) +{ + if (seat->wl_keyboard != NULL && seat->kbd.xkb_state != NULL) { + /* Seat has keyboard - use mouse bindings *with* modifiers */ + + const struct key_binding_set *bindings = + key_binding_for(term->wl->key_binding_manager, term->conf, seat); + xassert(bindings != NULL); + + xkb_mod_mask_t mods; + get_current_modifiers(seat, &mods, NULL, 0, true); + + /* Ignore selection override modifiers when + * matching modifiers */ + mods &= ~bindings->selection_overrides; + + const struct key_binding *match = NULL; + + tll_foreach(bindings->mouse, it) { + const struct key_binding *binding = &it->item; + + if (binding->m.button != button) { + /* Wrong button */ + continue; + } + + if (binding->mods != mods) { + /* Modifier mismatch */ + continue; + } + + if (binding->m.count > seat->mouse.count) { + /* Not correct click count */ + continue; + } + + if (match == NULL || binding->m.count > match->m.count) + match = binding; + } + + return match; + } + + else { + /* Seat does NOT have a keyboard - use mouse bindings *without* + * modifiers */ + const struct config_key_binding *match = NULL; + const struct config *conf = term->conf; + + for (size_t i = 0; i < conf->bindings.mouse.count; i++) { + const struct config_key_binding *binding = + &conf->bindings.mouse.arr[i]; + + if (binding->m.button != button) { + /* Wrong button */ + continue; + } + + if (binding->m.count > seat->mouse.count) { + /* Incorrect click count */ + continue; + } + + if (tll_length(binding->modifiers) > 0) { + /* Binding has modifiers */ + continue; + } + + if (match == NULL || binding->m.count > match->m.count) + match = binding; + } + + if (match != NULL) { + static struct key_binding bind; + bind.action = match->action; + bind.aux = &match->aux; + return &bind; + } + + return NULL; + } + + BUG("should not get here"); +} + +static void +wl_pointer_button(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, uint32_t time, uint32_t button, uint32_t state) +{ + LOG_DBG("BUTTON: pointer=%p, serial=%u, button=%x, state=%u", + (void *)wl_pointer, serial, button, state); + + xassert(serial != 0); + + struct seat *seat = data; + + /* Touch-emulated pointer events have wl_pointer == NULL. */ + if (wl_pointer != NULL && touch_is_active(seat)) + return; + + struct wayland *wayl = seat->wayl; + struct terminal *term = seat->mouse_focus; + + seat->pointer.serial = serial; + seat->pointer.hidden = false; + + xassert(term != NULL); + + enum term_surface surf_kind = TERM_SURF_NONE; + bool send_to_client = false; + + if (state == WL_POINTER_BUTTON_STATE_PRESSED) { + if (seat->wl_touch != NULL && seat->touch.state == TOUCH_STATE_IDLE) { + seat->touch.state = TOUCH_STATE_INHIBITED; + } + + /* Time since last click */ + struct timespec now, since_last; + clock_gettime(CLOCK_MONOTONIC, &now); + timespec_sub(&now, &seat->mouse.last_time, &since_last); + + if (seat->mouse.last_released_button == button && + since_last.tv_sec == 0 && since_last.tv_nsec <= 300 * 1000 * 1000) + { + seat->mouse.count++; + } else + seat->mouse.count = 1; + + /* + * Workaround GNOME bug + * + * Dragging the window, then stopping the drag (releasing the + * mouse button), *without* moving the mouse, and then + * clicking twice, waiting for the CSD timer, and finally + * clicking once more, results in the following sequence + * (keyboard and other irrelevant events filtered out, unless + * they're needed to prove a point): + * + * dbg: input.c:1551: cancelling drag timer, moving window + * dbg: input.c:759: keyboard_leave: keyboard=0x607000003580, serial=873, surface=0x6070000036d0 + * dbg: input.c:1432: seat0: pointer-leave: pointer=0x607000003660, serial=874, surface = 0x6070000396e0, old-moused = 0x622000006100 + * + * --> drag stopped here + * + * --> LMB clicked first time after the drag (generates the + * enter event on *release*, but no button events) + * dbg: input.c:1360: pointer-enter: pointer=0x607000003660, serial=876, surface = 0x6070000396e0, new-moused = 0x622000006100 + * + * --> LMB clicked, and held until the timer times out, second + * time after the drag + * dbg: input.c:1712: BUTTON: pointer=0x607000003660, serial=877, button=110, state=1 + * dbg: input.c:1806: starting move timer + * dbg: input.c:1692: move timer timed out + * dbg: input.c:759: keyboard_leave: keyboard=0x607000003580, serial=878, surface=0x6070000036d0 + * + * --> NOTE: ^^ no pointer leave event this time, only the + * keyboard leave + * + * --> LMB clicked one last time + * dbg: input.c:697: seat0: keyboard_enter: keyboard=0x607000003580, serial=879, surface=0x6070000036d0 + * dbg: input.c:1712: BUTTON: pointer=0x607000003660, serial=880, button=110, state=1 + * err: input.c:1741: BUG in wl_pointer_button(): assertion failed: 'it->item.button != button' + * + * What are we seeing? + * + * - GNOME does *not* send a pointer *enter* event after the drag + * has stopped + * - The second drag does *not* generate a pointer *leave* event + * - The missing leave event means we're still tracking LMB as + * being held down in our seat struct. + * - This leads to an assert (debug builds) when LMB is clicked + * again (seat's button list already contains LMB). + * + * Note: I've also observed variants of the above + */ + tll_foreach(seat->mouse.buttons, it) { + if (it->item.button == button) { + LOG_WARN("multiple button press events for button %d " + "(compositor bug?)", button); + tll_remove(seat->mouse.buttons, it); + break; + } + } + +#if defined(_DEBUG) + tll_foreach(seat->mouse.buttons, it) + xassert(it->item.button != button); +#endif + + /* + * Remember which surface "owns" this button, so that we can + * send motion and button release events to that surface, even + * if the pointer is no longer over it. + */ + tll_push_back( + seat->mouse.buttons, + ((struct button_tracker){ + .button = button, + .surf_kind = term->active_surface, + .send_to_client = false})); + + seat->mouse.last_time = now; + + surf_kind = term->active_surface; + send_to_client = false; /* For now, may be set to true if a binding consumes the button */ + } else { + bool UNUSED have_button = false; + tll_foreach(seat->mouse.buttons, it) { + if (it->item.button == button) { + have_button = true; + surf_kind = it->item.surf_kind; + send_to_client = it->item.send_to_client; + tll_remove(seat->mouse.buttons, it); + break; + } + } + + if (seat->wl_touch != NULL && seat->touch.state == TOUCH_STATE_INHIBITED) { + if (tll_length(seat->mouse.buttons) == 0) { + seat->touch.state = TOUCH_STATE_IDLE; + } + } + + if (!have_button) { + /* + * Seen on Sway with slurp + * + * 1. Run slurp + * 2. Press, and hold left mouse button + * 3. Press escape, to cancel slurp + * 4. Release mouse button + * 5. BAM! + */ + LOG_WARN("stray button release event (compositor bug?)"); + return; + } + + seat->mouse.last_released_button = button; + } + + switch (surf_kind) { + case TERM_SURF_TITLE: + if (state == WL_POINTER_BUTTON_STATE_PRESSED) { + + struct wl_window *win = term->window; + + /* Toggle maximized state on double-click */ + if (term->conf->csd.double_click_to_maximize && + button == BTN_LEFT && + seat->mouse.count == 2) + { + if (win->is_maximized) + xdg_toplevel_unset_maximized(win->xdg_toplevel); + else + xdg_toplevel_set_maximized(win->xdg_toplevel); + } + + else if (button == BTN_LEFT && win->csd.move_timeout_fd < 0) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 200000000}, + }; + + int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd >= 0 && + timerfd_settime(fd, 0, &timeout, NULL) == 0 && + fdm_add(wayl->fdm, fd, EPOLLIN, &fdm_csd_move, seat)) + { + win->csd.move_timeout_fd = fd; + win->csd.serial = serial; + } else { + LOG_ERRNO("failed to configure XDG toplevel move timer FD"); + if (fd >= 0) + close(fd); + } + } + + if (button == BTN_RIGHT && tll_length(seat->mouse.buttons) == 1) { + const struct csd_data info = get_csd_data(term, CSD_SURF_TITLE); + xdg_toplevel_show_window_menu( + win->xdg_toplevel, + seat->wl_seat, + seat->pointer.serial, + seat->mouse.x + info.x, seat->mouse.y + info.y); + } + } + + else if (state == WL_POINTER_BUTTON_STATE_RELEASED) { + struct wl_window *win = term->window; + if (win->csd.move_timeout_fd >= 0) { + fdm_del(wayl->fdm, win->csd.move_timeout_fd); + win->csd.move_timeout_fd = -1; + } + } + return; + + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: { + if (button == BTN_LEFT && state == WL_POINTER_BUTTON_STATE_PRESSED) { + enum xdg_toplevel_resize_edge resize_type = XDG_TOPLEVEL_RESIZE_EDGE_NONE; + + int x = seat->mouse.x; + int y = seat->mouse.y; + + if (is_top_left(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT; + else if (is_top_right(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT; + else if (is_bottom_left(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT; + else if (is_bottom_right(term, x, y)) + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT; + else { + if (term->active_surface == TERM_SURF_BORDER_LEFT && + !term->window->is_constrained_left) + { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_LEFT; + } + + else if (term->active_surface == TERM_SURF_BORDER_RIGHT && + !term->window->is_constrained_right) + { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_RIGHT; + } + + else if (term->active_surface == TERM_SURF_BORDER_TOP && + !term->window->is_constrained_top) + { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_TOP; + } + + else if (term->active_surface == TERM_SURF_BORDER_BOTTOM && + !term->window->is_constrained_bottom) + { + resize_type = XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM; + } + } + + if (resize_type != XDG_TOPLEVEL_RESIZE_EDGE_NONE) { + xdg_toplevel_resize( + term->window->xdg_toplevel, seat->wl_seat, serial, resize_type); + } + } + return; + } + + case TERM_SURF_BUTTON_MINIMIZE: + if (button == BTN_LEFT && + pointer_is_on_button(term, seat, CSD_SURF_MINIMIZE) && + state == WL_POINTER_BUTTON_STATE_RELEASED) + { + xdg_toplevel_set_minimized(term->window->xdg_toplevel); + } + break; + + case TERM_SURF_BUTTON_MAXIMIZE: + if (button == BTN_LEFT && + pointer_is_on_button(term, seat, CSD_SURF_MAXIMIZE) && + state == WL_POINTER_BUTTON_STATE_RELEASED) + { + if (term->window->is_maximized) + xdg_toplevel_unset_maximized(term->window->xdg_toplevel); + else + xdg_toplevel_set_maximized(term->window->xdg_toplevel); + } + break; + + case TERM_SURF_BUTTON_CLOSE: + if (button == BTN_LEFT && + pointer_is_on_button(term, seat, CSD_SURF_CLOSE) && + state == WL_POINTER_BUTTON_STATE_RELEASED) + { + term_shutdown(term); + } + break; + + case TERM_SURF_GRID: { + search_cancel(term); + urls_reset(term); + + bool cursor_is_on_grid = seat->mouse.col >= 0 && seat->mouse.row >= 0; + + switch (state) { + case WL_POINTER_BUTTON_STATE_PRESSED: { + bool consumed = false; + + if (cursor_is_on_grid && term_mouse_grabbed(term, seat)) { + const struct key_binding *match = + match_mouse_binding(seat, term, button); + + if (match != NULL) + consumed = execute_binding(seat, term, match, serial, 1); + } + + send_to_client = !consumed && cursor_is_on_grid; + + if (send_to_client) + tll_back(seat->mouse.buttons).send_to_client = true; + + if (send_to_client && + !term_mouse_grabbed(term, seat) && + cursor_is_on_grid) + { + term_mouse_down( + term, button, seat->mouse.row, seat->mouse.col, + seat->mouse.y - term->margins.top, + seat->mouse.x - term->margins.left, + seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); + } + break; + } + + case WL_POINTER_BUTTON_STATE_RELEASED: + selection_finalize(seat, term, serial); + + if (send_to_client && !term_mouse_grabbed(term, seat)) { + term_mouse_up( + term, button, seat->mouse.row, seat->mouse.col, + seat->mouse.y - term->margins.top, + seat->mouse.x - term->margins.left, + seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); + } + break; + } + break; + } + + case TERM_SURF_TAB_BAR: { + if (state != WL_POINTER_BUTTON_STATE_PRESSED) + break; + if (button != BTN_LEFT) + break; + + size_t idx = term_tab_bar_hit_test(term, seat->mouse.x, seat->mouse.y); + if (idx == SIZE_MAX || idx >= term->window->tab_count) + break; + if (idx == term->window->active_tab) + break; + term_tab_switch(term->window, idx); + break; + } + + case TERM_SURF_TAB_OVERVIEW: { + if (state != WL_POINTER_BUTTON_STATE_PRESSED) + break; + struct wl_window *win = term->window; + if (button == BTN_LEFT) { + int idx = tab_overview_hit_test(win, seat->mouse.x, seat->mouse.y); + if (idx >= 0 && (size_t)idx < win->tab_count) { + if ((size_t)idx != win->active_tab) + term_tab_switch(win, (size_t)idx); + /* Selecting a card snaps closed — no fade-out */ + tab_overview_close_instant(win); + } else { + /* Click on backdrop animates closed */ + tab_overview_toggle(win); + } + } else if (button == BTN_RIGHT) { + tab_overview_toggle(win); + } + break; + } + + case TERM_SURF_NONE: + BUG("Invalid surface type"); + break; + + } +} + +static void +alternate_scroll(struct seat *seat, int amount, int button) +{ + if (seat->wl_keyboard == NULL) + return; + + /* Should be cleared in leave event */ + xassert(seat->mouse_focus != NULL); + struct terminal *term = seat->mouse_focus; + + assert(button == BTN_BACK || button == BTN_FORWARD); + + xkb_keycode_t key = button == BTN_BACK + ? seat->kbd.key_arrow_up : seat->kbd.key_arrow_down; + + for (int i = 0; i < amount; i++) + key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_DOWN); + key_press_release(seat, term, seat->kbd.serial, key, XKB_KEY_UP); +} + +static void +mouse_scroll(struct seat *seat, int amount, enum wl_pointer_axis axis) +{ + struct terminal *term = seat->mouse_focus; + xassert(term != NULL); + + int button = axis == WL_POINTER_AXIS_VERTICAL_SCROLL + ? amount < 0 ? BTN_WHEEL_BACK : BTN_WHEEL_FORWARD + : amount < 0 ? BTN_WHEEL_LEFT : BTN_WHEEL_RIGHT; + amount = abs(amount); + + if (term_mouse_grabbed(term, seat)) { + seat->mouse.count = 1; + + const struct key_binding *match = + match_mouse_binding(seat, term, button); + + if (match != NULL) + execute_binding(seat, term, match, seat->pointer.serial, amount); + + seat->mouse.last_released_button = button; + } + + else if (seat->mouse.col >= 0 && seat->mouse.row >= 0) { + xassert(seat->mouse.col < term->cols); + xassert(seat->mouse.row < term->rows); + + for (int i = 0; i < amount; i++) { + term_mouse_down( + term, button, seat->mouse.row, seat->mouse.col, + seat->mouse.y - term->margins.top, + seat->mouse.x - term->margins.left, + seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); + } + + term_mouse_up( + term, button, seat->mouse.row, seat->mouse.col, + seat->mouse.y - term->margins.top, + seat->mouse.x - term->margins.left, + seat->kbd.shift, seat->kbd.alt, seat->kbd.ctrl); + } +} + +static double +mouse_scroll_multiplier(const struct terminal *term, const struct seat *seat) +{ + return (term->grid == &term->normal || + (term_mouse_grabbed(term, seat) && term->alt_scrolling)) + ? term->conf->scrollback.multiplier + : 1.0; +} + +static void +wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, + uint32_t time, uint32_t axis, wl_fixed_t value) +{ + struct seat *seat = data; + + if (touch_is_active(seat)) + return; + + if (seat->mouse.have_discrete) + return; + + xassert(seat->mouse_focus != NULL); + xassert(axis < ALEN(seat->mouse.aggregated)); + + const struct terminal *term = seat->mouse_focus; + + /* + * Aggregate scrolled amount until we get at least 1.0 + * + * Without this, very slow scrolling will never actually scroll + * anything. + */ + seat->mouse.aggregated[axis] += + mouse_scroll_multiplier(term, seat) * wl_fixed_to_double(value); + if (fabs(seat->mouse.aggregated[axis]) < seat->mouse_focus->cell_height) + return; + + int lines = seat->mouse.aggregated[axis] / seat->mouse_focus->cell_height; + mouse_scroll(seat, lines, axis); + seat->mouse.aggregated[axis] -= (double)lines * seat->mouse_focus->cell_height; +} + +static void +wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, + enum wl_pointer_axis axis, int32_t discrete) +{ + LOG_DBG("axis_discrete: %d", discrete); + struct seat *seat = data; + + if (touch_is_active(seat)) + return; + + seat->mouse.have_discrete = true; + int amount = discrete; + + if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) { + /* Treat mouse wheel left/right as regular buttons */ + } else + amount *= mouse_scroll_multiplier(seat->mouse_focus, seat); + + mouse_scroll(seat, amount, axis); +} + +#if defined(WL_POINTER_AXIS_VALUE120_SINCE_VERSION) +static void +wl_pointer_axis_value120(void *data, struct wl_pointer *wl_pointer, + enum wl_pointer_axis axis, int32_t value120) +{ + LOG_DBG("axis_value120: %d -> %.2f", value120, (float)value120 / 120.); + + struct seat *seat = data; + + if (touch_is_active(seat)) + return; + + seat->mouse.have_discrete = true; + + /* + * 120 corresponds to a single "low-res" scroll step. + * + * When doing high-res scrolling, take the scrollback.multiplier, + * and calculate how many degrees there are per line. + * + * For example, with scrollback.multiplier = 3, we have 120 / 3 == 40. + * + * Then, accumulate high-res scroll events, until we have *at + * least* that much. Translate the accumulated value to number of + * lines, and scroll. + * + * Subtract the "used" degrees from the accumulated value, and + * keep what's left (this value will always be less than the + * per-line value). + */ + const double multiplier = mouse_scroll_multiplier(seat->mouse_focus, seat); + const double per_line = 120. / multiplier; + + seat->mouse.aggregated_120[axis] += (double)value120; + + if (fabs(seat->mouse.aggregated_120[axis]) < per_line) + return; + + int lines = (int)(seat->mouse.aggregated_120[axis] / per_line); + mouse_scroll(seat, lines, axis); + seat->mouse.aggregated_120[axis] -= (double)lines * per_line; +} +#endif + +static void +wl_pointer_frame(void *data, struct wl_pointer *wl_pointer) +{ + struct seat *seat = data; + + if (touch_is_active(seat)) + return; + + seat->mouse.have_discrete = false; +} + +static void +wl_pointer_axis_source(void *data, struct wl_pointer *wl_pointer, + uint32_t axis_source) +{ +} + +static void +wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer, + uint32_t time, uint32_t axis) +{ + struct seat *seat = data; + + if (touch_is_active(seat)) + return; + + xassert(axis < ALEN(seat->mouse.aggregated)); + seat->mouse.aggregated[axis] = 0.; +} + +const struct wl_pointer_listener pointer_listener = { + .enter = &wl_pointer_enter, + .leave = &wl_pointer_leave, + .motion = &wl_pointer_motion, + .button = &wl_pointer_button, + .axis = &wl_pointer_axis, + .frame = &wl_pointer_frame, + .axis_source = &wl_pointer_axis_source, + .axis_stop = &wl_pointer_axis_stop, + .axis_discrete = &wl_pointer_axis_discrete, +#if defined(WL_POINTER_AXIS_VALUE120_SINCE_VERSION) + .axis_value120 = &wl_pointer_axis_value120, +#endif +}; + +static bool +touch_to_scroll(struct seat *seat, struct terminal *term, + wl_fixed_t surface_x, wl_fixed_t surface_y) +{ + bool coord_updated = false; + + int y = wl_fixed_to_int(surface_y) * term->scale; + int rows = (y - seat->mouse.y) / term->cell_height; + if (rows != 0) { + mouse_scroll(seat, -rows, WL_POINTER_AXIS_VERTICAL_SCROLL); + seat->mouse.y += rows * term->cell_height; + coord_updated = true; + } + + int x = wl_fixed_to_int(surface_x) * term->scale; + int cols = (x - seat->mouse.x) / term->cell_width; + if (cols != 0) { + mouse_scroll(seat, -cols, WL_POINTER_AXIS_HORIZONTAL_SCROLL); + seat->mouse.x += cols * term->cell_width; + coord_updated = true; + } + + return coord_updated; +} + +static void +wl_touch_down(void *data, struct wl_touch *wl_touch, uint32_t serial, + uint32_t time, struct wl_surface *surface, int32_t id, + wl_fixed_t surface_x, wl_fixed_t surface_y) +{ + struct seat *seat = data; + + if (seat->touch.state != TOUCH_STATE_IDLE) + return; + + struct wl_window *win = wl_surface_get_user_data(surface); + struct terminal *term = win->term; + + LOG_DBG("touch_down: touch=%p, x=%d, y=%d", (void *)wl_touch, + wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); + + int x = wl_fixed_to_int(surface_x) * term->scale; + int y = wl_fixed_to_int(surface_y) * term->scale; + + seat->mouse.x = x; + seat->mouse.y = y; + mouse_coord_pixel_to_cell(seat, term, x, y); + + seat->touch.state = TOUCH_STATE_HELD; + seat->touch.serial = serial; + seat->touch.time = time + term->conf->touch.long_press_delay; + seat->touch.surface = surface; + seat->touch.surface_kind = term_surface_kind(term, surface); + seat->touch.id = id; +} + +static void +wl_touch_up(void *data, struct wl_touch *wl_touch, uint32_t serial, + uint32_t time, int32_t id) +{ + struct seat *seat = data; + + if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id) + return; + + LOG_DBG("touch_up: touch=%p", (void *)wl_touch); + + struct wl_window *win = wl_surface_get_user_data(seat->touch.surface); + struct terminal *term = win->term; + + struct terminal *old_term = seat->mouse_focus; + enum term_surface old_active_surface = term->active_surface; + seat->mouse_focus = term; + term->active_surface = seat->touch.surface_kind; + + switch (seat->touch.state) { + case TOUCH_STATE_HELD: + wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT, + WL_POINTER_BUTTON_STATE_PRESSED); + /* fallthrough */ + case TOUCH_STATE_DRAGGING: + wl_pointer_button(seat, NULL, serial, time, BTN_LEFT, + WL_POINTER_BUTTON_STATE_RELEASED); + /* fallthrough */ + case TOUCH_STATE_SCROLLING: + term->active_surface = TERM_SURF_NONE; + seat->touch.state = TOUCH_STATE_IDLE; + break; + + case TOUCH_STATE_INHIBITED: + case TOUCH_STATE_IDLE: + BUG("Bad touch state: %d", seat->touch.state); + break; + } + + seat->mouse_focus = old_term; + term->active_surface = old_active_surface; +} + +static void +wl_touch_motion(void *data, struct wl_touch *wl_touch, uint32_t time, + int32_t id, wl_fixed_t surface_x, wl_fixed_t surface_y) +{ + struct seat *seat = data; + if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id) + return; + + LOG_DBG("touch_motion: touch=%p, x=%d, y=%d", (void *)wl_touch, + wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y)); + + struct wl_window *win = wl_surface_get_user_data(seat->touch.surface); + struct terminal *term = win->term; + + struct terminal *old_term = seat->mouse_focus; + enum term_surface old_active_surface = term->active_surface; + seat->mouse_focus = term; + term->active_surface = seat->touch.surface_kind; + + switch (seat->touch.state) { + case TOUCH_STATE_HELD: + if (time <= seat->touch.time && term->active_surface == TERM_SURF_GRID) { + if (touch_to_scroll(seat, term, surface_x, surface_y)) + seat->touch.state = TOUCH_STATE_SCROLLING; + break; + } else { + wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT, + WL_POINTER_BUTTON_STATE_PRESSED); + seat->touch.state = TOUCH_STATE_DRAGGING; + /* fallthrough */ + } + case TOUCH_STATE_DRAGGING: + wl_pointer_motion(seat, NULL, time, surface_x, surface_y); + break; + case TOUCH_STATE_SCROLLING: + touch_to_scroll(seat, term, surface_x, surface_y); + break; + + case TOUCH_STATE_INHIBITED: + case TOUCH_STATE_IDLE: + BUG("Bad touch state: %d", seat->touch.state); + break; + } + + seat->mouse_focus = old_term; + term->active_surface = old_active_surface; +} + +static void +wl_touch_frame(void *data, struct wl_touch *wl_touch) +{ +} + +static void +wl_touch_cancel(void *data, struct wl_touch *wl_touch) +{ + struct seat *seat = data; + if (seat->touch.state == TOUCH_STATE_INHIBITED) + return; + + seat->touch.state = TOUCH_STATE_IDLE; +} + +const struct wl_touch_listener touch_listener = { + .down = wl_touch_down, + .up = wl_touch_up, + .motion = wl_touch_motion, + .frame = wl_touch_frame, + .cancel = wl_touch_cancel, +}; diff --git a/input.h b/input.h new file mode 100644 index 0000000..34342bb --- /dev/null +++ b/input.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include "cursor-shape.h" +#include "misc.h" +#include "wayland.h" + +/* + * Custom defines for mouse wheel left/right buttons. + * + * Libinput does not define these. On Wayland, all scroll events (both + * vertical and horizontal) are reported not as buttons, as 'axis' + * events. + * + * Libinput _does_ define BTN_BACK and BTN_FORWARD, which is + * what we use for vertical scroll events. But for horizontal scroll + * events, there aren't any pre-defined mouse buttons. + * + * Mouse buttons are in the range 0x110 - 0x11f, with joystick defines + * starting at 0x120. + */ +#define BTN_WHEEL_BACK 0x11c +#define BTN_WHEEL_FORWARD 0x11d +#define BTN_WHEEL_LEFT 0x11e +#define BTN_WHEEL_RIGHT 0x11f + +extern const struct wl_keyboard_listener keyboard_listener; +extern const struct wl_pointer_listener pointer_listener; +extern const struct wl_touch_listener touch_listener; + +void input_repeat(struct seat *seat, uint32_t key); + +void get_current_modifiers(const struct seat *seat, + xkb_mod_mask_t *effective, + xkb_mod_mask_t *consumed, + uint32_t key, bool filter_locked); + +enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y); diff --git a/key-binding.c b/key-binding.c new file mode 100644 index 0000000..a2883ed --- /dev/null +++ b/key-binding.c @@ -0,0 +1,661 @@ +#include "key-binding.h" + +#include + +#define LOG_MODULE "key-binding" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "config.h" +#include "debug.h" +#include "terminal.h" +#include "util.h" +#include "wayland.h" +#include "xkbcommon-vmod.h" +#include "xmalloc.h" + +struct vmod_map { + const char *name; + xkb_mod_mask_t virtual_mask; + xkb_mod_mask_t real_mask; +}; + +struct key_set { + struct key_binding_set public; + + const struct config *conf; + const struct seat *seat; + size_t conf_ref_count; + + /* Virtual to real modifier mappings */ + struct vmod_map vmods[8]; +}; +typedef tll(struct key_set) bind_set_list_t; + +struct key_binding_manager { + struct key_set *last_used_set; + bind_set_list_t binding_sets; +}; + +static void load_keymap(struct key_set *set); +static void unload_keymap(struct key_set *set); + +struct key_binding_manager * +key_binding_manager_new(void) +{ + struct key_binding_manager *mgr = xcalloc(1, sizeof(*mgr)); + return mgr; +} + +void +key_binding_manager_destroy(struct key_binding_manager *mgr) +{ + xassert(tll_length(mgr->binding_sets) == 0); + free(mgr); +} + +static void +initialize_vmod_mappings(struct key_set *set) +{ + if (set->seat == NULL || set->seat->kbd.xkb_keymap == NULL) + return; + + set->vmods[0].name = XKB_VMOD_NAME_ALT; + set->vmods[1].name = XKB_VMOD_NAME_HYPER; + set->vmods[2].name = XKB_VMOD_NAME_LEVEL3; + set->vmods[3].name = XKB_VMOD_NAME_LEVEL5; + set->vmods[4].name = XKB_VMOD_NAME_META; + set->vmods[5].name = XKB_VMOD_NAME_NUM; + set->vmods[6].name = XKB_VMOD_NAME_SCROLL; + set->vmods[7].name = XKB_VMOD_NAME_SUPER; + + struct xkb_state *scratch_state = xkb_state_new(set->seat->kbd.xkb_keymap); + xassert(scratch_state != NULL); + + for (size_t i = 0; i < ALEN(set->vmods); i++) { + xkb_mod_index_t virt_idx = xkb_keymap_mod_get_index( + set->seat->kbd.xkb_keymap, set->vmods[i].name); + + if (virt_idx != XKB_MOD_INVALID) { + xkb_mod_mask_t vmask = 1 << virt_idx; + xkb_state_update_mask(scratch_state, vmask, 0, 0, 0, 0, 0); + set->vmods[i].real_mask = xkb_state_serialize_mods( + scratch_state, XKB_STATE_MODS_DEPRESSED) & ~vmask; + set->vmods[i].virtual_mask = vmask; + + LOG_DBG("%s: 0x%04x -> 0x%04x", + set->vmods[i].name, + set->vmods[i].virtual_mask, + set->vmods[i].real_mask); + } else { + set->vmods[i].virtual_mask = 0; + set->vmods[i].real_mask = 0; + + LOG_DBG("%s: virtual modifier not available", set->vmods[i].name); + } + } + + xkb_state_unref(scratch_state); +} + +void +key_binding_new_for_seat(struct key_binding_manager *mgr, + const struct seat *seat) +{ +#if defined(_DEBUG) + tll_foreach(mgr->binding_sets, it) + xassert(it->item.seat != seat); +#endif + + tll_foreach(seat->wayl->terms, it) { + struct key_set set = { + .public = { + .key = tll_init(), + .search = tll_init(), + .url = tll_init(), + .mouse = tll_init(), + }, + .conf = it->item->conf, + .seat = seat, + .conf_ref_count = 1, + }; + + tll_push_back(mgr->binding_sets, set); + initialize_vmod_mappings(&tll_back(mgr->binding_sets)); + + LOG_DBG("new (seat): set=%p, seat=%p, conf=%p, ref-count=1", + (void *)&tll_back(mgr->binding_sets), + (void *)set.seat, (void *)set.conf); + + load_keymap(&tll_back(mgr->binding_sets)); + } + + LOG_DBG("new (seat): total number of sets: %zu", + tll_length(mgr->binding_sets)); +} + +void +key_binding_new_for_conf(struct key_binding_manager *mgr, + const struct wayland *wayl, const struct config *conf) +{ + tll_foreach(wayl->seats, it) { + struct seat *seat = &it->item; + + struct key_set *existing = + (struct key_set *)key_binding_for(mgr, conf, seat); + + if (existing != NULL) { + existing->conf_ref_count++; + continue; + } + + struct key_set set = { + .public = { + .key = tll_init(), + .search = tll_init(), + .url = tll_init(), + .mouse = tll_init(), + }, + .conf = conf, + .seat = seat, + .conf_ref_count = 1, + }; + + tll_push_back(mgr->binding_sets, set); + initialize_vmod_mappings(&tll_back(mgr->binding_sets)); + + load_keymap(&tll_back(mgr->binding_sets)); + + /* Chances are high this set will be requested next */ + mgr->last_used_set = &tll_back(mgr->binding_sets); + + LOG_DBG("new (conf): set=%p, seat=%p, conf=%p, ref-count=1", + (void *)&tll_back(mgr->binding_sets), + (void *)set.seat, (void *)set.conf); + } + + LOG_DBG("new (conf): total number of sets: %zu", + tll_length(mgr->binding_sets)); +} + +struct key_binding_set * NOINLINE +key_binding_for(struct key_binding_manager *mgr, const struct config *conf, + const struct seat *seat) +{ + struct key_set *last_used = mgr->last_used_set; + if (last_used != NULL && + last_used->conf == conf && + last_used->seat == seat) + { + // LOG_DBG("lookup: last used"); + return &last_used->public; + } + + tll_foreach(mgr->binding_sets, it) { + struct key_set *set = &it->item; + + if (set->conf != conf) + continue; + if (set->seat != seat) + continue; + +#if 0 + LOG_DBG("lookup: set=%p, seat=%p, conf=%p, ref-count=%zu", + (void *)set, (void *)seat, (void *)conf, set->conf_ref_count); +#endif + mgr->last_used_set = set; + return &set->public; + } + + return NULL; +} + +static void +key_binding_set_destroy(struct key_binding_manager *mgr, + struct key_set *set) +{ + unload_keymap(set); + if (mgr->last_used_set == set) + mgr->last_used_set = NULL; + + /* Note: caller must remove from binding_sets */ +} + +void +key_binding_remove_seat(struct key_binding_manager *mgr, + const struct seat *seat) +{ + tll_foreach(mgr->binding_sets, it) { + struct key_set *set = &it->item; + + if (set->seat != seat) + continue; + + key_binding_set_destroy(mgr, set); + tll_remove(mgr->binding_sets, it); + + LOG_DBG("remove seat: set=%p, seat=%p, total number of sets: %zu", + (void *)set, (void *)seat, tll_length(mgr->binding_sets)); + } + + LOG_DBG("remove seat: total number of sets: %zu", + tll_length(mgr->binding_sets)); +} + +void +key_binding_unref(struct key_binding_manager *mgr, const struct config *conf) +{ + tll_foreach(mgr->binding_sets, it) { + struct key_set *set = &it->item; + + if (set->conf != conf) + continue; + + xassert(set->conf_ref_count > 0); + if (--set->conf_ref_count == 0) { + LOG_DBG("unref conf: set=%p, seat=%p, conf=%p", + (void *)set, (void *)set->seat, (void *)conf); + + key_binding_set_destroy(mgr, set); + tll_remove(mgr->binding_sets, it); + } + } + + LOG_DBG("unref conf: total number of sets: %zu", + tll_length(mgr->binding_sets)); +} + +static xkb_keycode_list_t +key_codes_for_xkb_sym(struct xkb_keymap *keymap, xkb_keysym_t sym) +{ + xkb_keycode_list_t key_codes = tll_init(); + + /* + * Find all key codes that map to this symbol. + * + * This allows us to match bindings in other layouts + * too. + */ + struct xkb_state *state = xkb_state_new(keymap); + + for (xkb_keycode_t code = xkb_keymap_min_keycode(keymap); + code <= xkb_keymap_max_keycode(keymap); + code++) + { + if (xkb_state_key_get_one_sym(state, code) == sym) + tll_push_back(key_codes, code); + } + + xkb_state_unref(state); + return key_codes; +} + +static xkb_keysym_t +maybe_repair_key_combo(const struct seat *seat, + xkb_keysym_t sym, xkb_mod_mask_t mods) +{ + /* + * Detect combos containing a shifted symbol and the corresponding + * modifier, and replace the shifted symbol with its unshifted + * variant. + * + * For example, the combo is "Control+Shift+U". In this case, + * Shift is the modifier used to "shift" 'u' to 'U', after which + * 'Shift' will have been "consumed". Since we filter out consumed + * modifiers when matching key combos, this key combo will never + * trigger (we will never be able to match the 'Shift' modifier). + * + * There are two correct variants of the above key combo: + * - "Control+U" (upper case 'U') + * - "Control+Shift+u" (lower case 'u') + * + * What we do here is, for each key *code*, check if there are any + * (shifted) levels where it produces 'sym'. If there are, check + * *which* sets of modifiers are needed to produce it, and compare + * with 'mods'. + * + * If there is at least one common modifier, it means 'sym' is a + * "shifted" symbol, with the corresponding shifting modifier + * explicitly included in the key combo. I.e. the key combo will + * never trigger. + * + * We then proceed and "repair" the key combo by replacing 'sym' + * with the corresponding unshifted symbol. + * + * To reduce the noise, we ignore all key codes where the shifted + * symbol is the same as the unshifted symbol. + */ + + for (xkb_keycode_t code = xkb_keymap_min_keycode(seat->kbd.xkb_keymap); + code <= xkb_keymap_max_keycode(seat->kbd.xkb_keymap); + code++) + { + xkb_layout_index_t layout_idx = + xkb_state_key_get_layout(seat->kbd.xkb_state, code); + + /* Get all unshifted symbols for this key */ + const xkb_keysym_t *base_syms = NULL; + size_t base_count = xkb_keymap_key_get_syms_by_level( + seat->kbd.xkb_keymap, code, layout_idx, 0, &base_syms); + + if (base_count == 0 || sym == base_syms[0]) { + /* No unshifted symbols, or unshifted symbol is same as 'sym' */ + continue; + } + + /* Name of the unshifted symbol, for logging */ + char base_name[100]; + xkb_keysym_get_name(base_syms[0], base_name, sizeof(base_name)); + + /* Iterate all shift levels */ + for (xkb_level_index_t level_idx = 1; + level_idx < xkb_keymap_num_levels_for_key( + seat->kbd.xkb_keymap, code, layout_idx); + level_idx++) { + + /* Get all symbols for current shift level */ + const xkb_keysym_t *shifted_syms = NULL; + size_t shifted_count = xkb_keymap_key_get_syms_by_level( + seat->kbd.xkb_keymap, code, + layout_idx, level_idx, &shifted_syms); + + for (size_t i = 0; i < shifted_count; i++) { + if (shifted_syms[i] != sym) + continue; + + /* Get modifier sets that produces the current shift level */ + xkb_mod_mask_t mod_masks[16]; + size_t mod_mask_count = xkb_keymap_key_get_mods_for_level( + seat->kbd.xkb_keymap, code, layout_idx, level_idx, + mod_masks, ALEN(mod_masks)); + + /* Check if key combo's modifier set intersects */ + for (size_t j = 0; j < mod_mask_count; j++) { + if ((mod_masks[j] & mods) != mod_masks[j]) + continue; + + char combo[64] = {0}; + + for (int k = 0; k < sizeof(xkb_mod_mask_t) * 8; k++) { + if (!(mods & (1u << k))) + continue; + + const char *mod_name = xkb_keymap_mod_get_name( + seat->kbd.xkb_keymap, k); + strcat(combo, mod_name); + strcat(combo, "+"); + } + + size_t len = strlen(combo); + xkb_keysym_get_name( + sym, &combo[len], sizeof(combo) - len); + + LOG_WARN( + "%s: combo with both explicit modifier and shifted symbol " + "(level=%d, mod-mask=0x%08x), " + "replacing with %s", + combo, level_idx, mod_masks[j], base_name); + + /* Replace with unshifted symbol */ + return base_syms[0]; + } + } + } + } + + return sym; +} + +static int +key_cmp(struct key_binding a, struct key_binding b) +{ + xassert(a.type == b.type); + + /* + * Sort bindings such that bindings with the same symbol are + * sorted with the binding having the most modifiers comes first. + * + * This fixes an issue where the "wrong" key binding are triggered + * when used with "consumed" modifiers. + * + * For example: if Control+BackSpace is bound before + * Control+Shift+BackSpace, then the latter binding is never + * triggered. + * + * Why? Because Shift is a consumed modifier. This means + * Control+BackSpace is "the same" as Control+Shift+BackSpace. + * + * By sorting bindings with more modifiers first, we work around + * the problem. But note that it is *just* a workaround, and I'm + * not confident there aren't cases where it doesn't work. + * + * See https://codeberg.org/dnkl/foot/issues/1280 + */ + + const int a_mod_count = __builtin_popcount(a.mods); + const int b_mod_count = __builtin_popcount(b.mods); + + switch (a.type) { + case KEY_BINDING: + if (a.k.sym != b.k.sym) + return b.k.sym - a.k.sym; + return b_mod_count - a_mod_count; + + case MOUSE_BINDING: { + if (a.m.button != b.m.button) + return b.m.button - a.m.button; + if (a_mod_count != b_mod_count) + return b_mod_count - a_mod_count; + return b.m.count - a.m.count; + } + } + + BUG("invalid key binding type"); + return 0; +} + +static void NOINLINE +sort_binding_list(key_binding_list_t *list) +{ + tll_sort(*list, key_cmp); +} + +static xkb_mod_mask_t +mods_to_mask(const struct seat *seat, + const struct vmod_map *vmods, size_t vmod_count, + const config_modifier_list_t *mods) +{ + xkb_mod_mask_t mask = 0; + tll_foreach(*mods, it) { + const xkb_mod_index_t idx = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, it->item); + + if (idx == XKB_MOD_INVALID) { + LOG_ERR("%s: invalid modifier name", it->item); + continue; + } + + xkb_mod_mask_t mod = 1 << idx; + + /* Check if this is a virtual modifier, and if so, use the + real modifier it maps to instead */ + for (size_t i = 0; i < vmod_count; i++) { + if (vmods[i].virtual_mask == mod) { + mask |= vmods[i].real_mask; + mod = 0; + + LOG_DBG("%s: virtual modifier, mapped to 0x%04x", + it->item, vmods[i].real_mask); + break; + } + } + + mask |= mod; + } + + return mask; +} + +static void NOINLINE +convert_key_binding(struct key_set *set, + const struct config_key_binding *conf_binding, + key_binding_list_t *bindings) +{ + const struct seat *seat = set->seat; + + xkb_mod_mask_t mods = mods_to_mask( + seat, set->vmods, ALEN(set->vmods), &conf_binding->modifiers); + xkb_keysym_t sym = maybe_repair_key_combo(seat, conf_binding->k.sym, mods); + + struct key_binding binding = { + .type = KEY_BINDING, + .action = conf_binding->action, + .aux = &conf_binding->aux, + .mods = mods, + .k = { + .sym = sym, + .key_codes = key_codes_for_xkb_sym(seat->kbd.xkb_keymap, sym), + }, + }; + tll_push_back(*bindings, binding); + sort_binding_list(bindings); +} + +static void +convert_key_bindings(struct key_set *set) +{ + const struct config *conf = set->conf; + + for (size_t i = 0; i < conf->bindings.key.count; i++) { + const struct config_key_binding *binding = &conf->bindings.key.arr[i]; + convert_key_binding(set, binding, &set->public.key); + } +} + +static void +convert_search_bindings(struct key_set *set) +{ + const struct config *conf = set->conf; + + for (size_t i = 0; i < conf->bindings.search.count; i++) { + const struct config_key_binding *binding = &conf->bindings.search.arr[i]; + convert_key_binding(set, binding, &set->public.search); + } +} + +static void +convert_url_bindings(struct key_set *set) +{ + const struct config *conf = set->conf; + + for (size_t i = 0; i < conf->bindings.url.count; i++) { + const struct config_key_binding *binding = &conf->bindings.url.arr[i]; + convert_key_binding(set, binding, &set->public.url); + } +} + +static void +convert_mouse_binding(struct key_set *set, + const struct config_key_binding *conf_binding) +{ + struct key_binding binding = { + .type = MOUSE_BINDING, + .action = conf_binding->action, + .aux = &conf_binding->aux, + .mods = mods_to_mask(set->seat, set->vmods, ALEN(set->vmods), &conf_binding->modifiers), + .m = { + .button = conf_binding->m.button, + .count = conf_binding->m.count, + }, + }; + tll_push_back(set->public.mouse, binding); + sort_binding_list(&set->public.mouse); +} + +static void +convert_mouse_bindings(struct key_set *set) +{ + const struct config *conf = set->conf; + + for (size_t i = 0; i < conf->bindings.mouse.count; i++) { + const struct config_key_binding *binding = + &conf->bindings.mouse.arr[i]; + convert_mouse_binding(set, binding); + } +} + +static void NOINLINE +load_keymap(struct key_set *set) +{ + LOG_DBG("load keymap: set=%p, seat=%p, conf=%p", + (void *)set, (void *)set->seat, (void *)set->conf); + + if (set->seat->kbd.xkb_state == NULL || + set->seat->kbd.xkb_keymap == NULL) + { + LOG_DBG("no XKB keymap"); + return; + } + + convert_key_bindings(set); + convert_search_bindings(set); + convert_url_bindings(set); + convert_mouse_bindings(set); + + set->public.selection_overrides = mods_to_mask( + set->seat, set->vmods, ALEN(set->vmods), + &set->conf->mouse.selection_override_modifiers); +} + +void +key_binding_load_keymap(struct key_binding_manager *mgr, + const struct seat *seat) +{ + tll_foreach(mgr->binding_sets, it) { + struct key_set *set = &it->item; + + if (set->seat == seat) { + initialize_vmod_mappings(set); + load_keymap(set); + } + } +} + +static void NOINLINE +key_bindings_destroy(key_binding_list_t *bindings) +{ + tll_foreach(*bindings, it) { + struct key_binding *bind = &it->item; + switch (bind->type) { + case KEY_BINDING: tll_free(it->item.k.key_codes); break; + case MOUSE_BINDING: break; + } + + tll_remove(*bindings, it); + } +} + +static void NOINLINE +unload_keymap(struct key_set *set) +{ + key_bindings_destroy(&set->public.key); + key_bindings_destroy(&set->public.search); + key_bindings_destroy(&set->public.url); + key_bindings_destroy(&set->public.mouse); + set->public.selection_overrides = 0; +} + +void +key_binding_unload_keymap(struct key_binding_manager *mgr, + const struct seat *seat) +{ + tll_foreach(mgr->binding_sets, it) { + struct key_set *set = &it->item; + if (set->seat != seat) + continue; + + LOG_DBG("unload keymap: set=%p, seat=%p, conf=%p", + (void *)set, (void *)seat, (void *)set->conf); + + unload_keymap(set); + } +} diff --git a/key-binding.h b/key-binding.h new file mode 100644 index 0000000..45702ee --- /dev/null +++ b/key-binding.h @@ -0,0 +1,200 @@ +#pragma once + +#include + +#include +#include + +#include "config.h" + +enum bind_action_normal { + BIND_ACTION_NONE, + BIND_ACTION_NOOP, + BIND_ACTION_SCROLLBACK_UP_PAGE, + BIND_ACTION_SCROLLBACK_UP_HALF_PAGE, + BIND_ACTION_SCROLLBACK_UP_LINE, + BIND_ACTION_SCROLLBACK_DOWN_PAGE, + BIND_ACTION_SCROLLBACK_DOWN_HALF_PAGE, + BIND_ACTION_SCROLLBACK_DOWN_LINE, + BIND_ACTION_SCROLLBACK_HOME, + BIND_ACTION_SCROLLBACK_END, + BIND_ACTION_CLIPBOARD_COPY, + BIND_ACTION_CLIPBOARD_PASTE, + BIND_ACTION_PRIMARY_PASTE, + BIND_ACTION_SEARCH_START, + BIND_ACTION_FONT_SIZE_UP, + BIND_ACTION_FONT_SIZE_DOWN, + BIND_ACTION_FONT_SIZE_RESET, + BIND_ACTION_SPAWN_TERMINAL, + BIND_ACTION_MINIMIZE, + BIND_ACTION_MAXIMIZE, + BIND_ACTION_FULLSCREEN, + BIND_ACTION_PIPE_SCROLLBACK, + BIND_ACTION_PIPE_VIEW, + BIND_ACTION_PIPE_SELECTED, + BIND_ACTION_PIPE_COMMAND_OUTPUT, + BIND_ACTION_SHOW_URLS_COPY, + BIND_ACTION_SHOW_URLS_LAUNCH, + BIND_ACTION_SHOW_URLS_PERSISTENT, + BIND_ACTION_TEXT_BINDING, + BIND_ACTION_PROMPT_PREV, + BIND_ACTION_PROMPT_NEXT, + BIND_ACTION_UNICODE_INPUT, + BIND_ACTION_QUIT, + BIND_ACTION_REGEX_LAUNCH, + BIND_ACTION_REGEX_COPY, + BIND_ACTION_THEME_SWITCH_1, + BIND_ACTION_THEME_SWITCH_2, + BIND_ACTION_THEME_SWITCH_DARK, + BIND_ACTION_THEME_SWITCH_LIGHT, + BIND_ACTION_THEME_TOGGLE, + + /* Tab actions */ + BIND_ACTION_TAB_NEW, + BIND_ACTION_TAB_CLOSE, + BIND_ACTION_TAB_NEXT, + BIND_ACTION_TAB_PREV, + BIND_ACTION_TAB_1, + BIND_ACTION_TAB_2, + BIND_ACTION_TAB_3, + BIND_ACTION_TAB_4, + BIND_ACTION_TAB_5, + BIND_ACTION_TAB_6, + BIND_ACTION_TAB_7, + BIND_ACTION_TAB_8, + BIND_ACTION_TAB_9, + BIND_ACTION_TAB_OVERVIEW, + + /* Mouse specific actions - i.e. they require a mouse coordinate */ + BIND_ACTION_SCROLLBACK_UP_MOUSE, + BIND_ACTION_SCROLLBACK_DOWN_MOUSE, + BIND_ACTION_SELECT_BEGIN, + BIND_ACTION_SELECT_BEGIN_BLOCK, + BIND_ACTION_SELECT_EXTEND, + BIND_ACTION_SELECT_EXTEND_CHAR_WISE, + BIND_ACTION_SELECT_WORD, + BIND_ACTION_SELECT_WORD_WS, + BIND_ACTION_SELECT_QUOTE, + BIND_ACTION_SELECT_ROW, + + BIND_ACTION_KEY_COUNT = BIND_ACTION_TAB_OVERVIEW + 1, + BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1, +}; + +enum bind_action_search { + BIND_ACTION_SEARCH_NONE, + BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE, + BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE, + BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE, + BIND_ACTION_SEARCH_SCROLLBACK_HOME, + BIND_ACTION_SEARCH_SCROLLBACK_END, + BIND_ACTION_SEARCH_CANCEL, + BIND_ACTION_SEARCH_COMMIT, + BIND_ACTION_SEARCH_FIND_PREV, + BIND_ACTION_SEARCH_FIND_NEXT, + BIND_ACTION_SEARCH_EDIT_LEFT, + BIND_ACTION_SEARCH_EDIT_LEFT_WORD, + BIND_ACTION_SEARCH_EDIT_RIGHT, + BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, + BIND_ACTION_SEARCH_EDIT_HOME, + BIND_ACTION_SEARCH_EDIT_END, + BIND_ACTION_SEARCH_DELETE_PREV, + BIND_ACTION_SEARCH_DELETE_PREV_WORD, + BIND_ACTION_SEARCH_DELETE_NEXT, + BIND_ACTION_SEARCH_DELETE_NEXT_WORD, + BIND_ACTION_SEARCH_DELETE_TO_START, + BIND_ACTION_SEARCH_DELETE_TO_END, + BIND_ACTION_SEARCH_EXTEND_CHAR, + BIND_ACTION_SEARCH_EXTEND_WORD, + BIND_ACTION_SEARCH_EXTEND_WORD_WS, + BIND_ACTION_SEARCH_EXTEND_LINE_DOWN, + BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR, + BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD, + BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS, + BIND_ACTION_SEARCH_EXTEND_LINE_UP, + BIND_ACTION_SEARCH_CLIPBOARD_PASTE, + BIND_ACTION_SEARCH_PRIMARY_PASTE, + BIND_ACTION_SEARCH_UNICODE_INPUT, + BIND_ACTION_SEARCH_TOGGLE_CASE, + BIND_ACTION_SEARCH_TOGGLE_WHOLE_WORD, + BIND_ACTION_SEARCH_TOGGLE_REGEX, + BIND_ACTION_SEARCH_HISTORY_PREV, + BIND_ACTION_SEARCH_HISTORY_NEXT, + BIND_ACTION_SEARCH_COMMIT_LINE, + BIND_ACTION_SEARCH_COUNT, +}; + +enum bind_action_url { + BIND_ACTION_URL_NONE, + BIND_ACTION_URL_CANCEL, + BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL, + BIND_ACTION_URL_COUNT, +}; + +typedef tll(xkb_keycode_t) xkb_keycode_list_t; + +struct key_binding { + enum key_binding_type type; + + int action; /* enum bind_action_* */ + xkb_mod_mask_t mods; + + union { + struct { + xkb_keysym_t sym; + xkb_keycode_list_t key_codes; + } k; + + struct { + uint32_t button; + int count; + } m; + }; + + const struct binding_aux *aux; +}; +typedef tll(struct key_binding) key_binding_list_t; + +struct terminal; +struct seat; +struct wayland; + +struct key_binding_set { + key_binding_list_t key; + key_binding_list_t search; + key_binding_list_t url; + key_binding_list_t mouse; + xkb_mod_mask_t selection_overrides; +}; + +struct key_binding_manager; + +struct key_binding_manager *key_binding_manager_new(void); +void key_binding_manager_destroy(struct key_binding_manager *mgr); + +void key_binding_new_for_seat( + struct key_binding_manager *mgr, const struct seat *seat); + +void key_binding_new_for_conf( + struct key_binding_manager *mgr, const struct wayland *wayl, + const struct config *conf); + +/* Returns the set of key bindings associated with this seat/conf pair */ +struct key_binding_set *key_binding_for( + struct key_binding_manager *mgr, const struct config *conf, + const struct seat *seat); + +/* Remove all key bindings tied to the specified seat */ +void key_binding_remove_seat( + struct key_binding_manager *mgr, const struct seat *seat); + +void key_binding_unref( + struct key_binding_manager *mgr, const struct config *conf); + +void key_binding_load_keymap( + struct key_binding_manager *mgr, const struct seat *seat); +void key_binding_unload_keymap( + struct key_binding_manager *mgr, const struct seat *seat); diff --git a/keymap.h b/keymap.h new file mode 100644 index 0000000..008f0e1 --- /dev/null +++ b/keymap.h @@ -0,0 +1,414 @@ +#pragma once + +#include + +#include "terminal.h" + +enum modifier { + MOD_NONE = 0x0, + MOD_ANY = 0x1, + MOD_SHIFT = 0x2, + MOD_ALT = 0x4, + MOD_CTRL = 0x8, + MOD_META = 0x10, + MOD_MODIFY_OTHER_KEYS_STATE1 = 0x20, + MOD_MODIFY_OTHER_KEYS_STATE2 = 0x40, +}; + +struct key_data { + enum modifier modifiers; + enum cursor_keys cursor_keys_mode; + enum keypad_keys keypad_keys_mode; + const char *seq; +}; + +static const struct key_data key_escape[] = { + {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;27~"}, + {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\033"}, + {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;27~"}, + {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;5;27~"}, + {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;27~"}, + {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;27~"}, + {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;27~"}, + {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;27~"}, + {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;27~"}, + {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;27~"}, + {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;27~"}, + {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;27~"}, + {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;27~"}, + {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;27~"}, + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;27~"}, + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033"}, +}; + +static const struct key_data key_return[] = { + {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;13~"}, + {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\r"}, + {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;3;13~"}, + {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;13~"}, + {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;5;13~"}, + {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;13~"}, + {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;13~"}, + {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;13~"}, + {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;13~"}, + {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;13~"}, + {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;13~"}, + {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;13~"}, + {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;13~"}, + {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;13~"}, + {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;13~"}, + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;13~"}, + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\r"}, +}; + +/* Tab isn't covered by the regular "modifyOtherKeys" handling */ +static const struct key_data key_tab[] = { + {MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[Z"}, + {MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;9~"}, + {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\t"}, + {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;3;9~"}, + {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;9~"}, + {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;5;9~"}, + {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;9~"}, + {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;9~"}, + {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;9~"}, + {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;9~"}, + {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;9~"}, + {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;9~"}, + {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;9~"}, + {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;9~"}, + {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;9~"}, + {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;9~"}, + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;9~"}, + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\t"}, +}; + +/* + * Shift+Tab produces ISO_Left_Tab + * + * However, all combos (except Shift+Tab) acts as if we pressed + * mods+shift+tab. + */ +static const struct key_data key_iso_left_tab[] = { + {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;9~"}, + {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;9~"}, + {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;9~"}, + {MOD_SHIFT | MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;9~"}, + {MOD_SHIFT | MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;9~"}, + {MOD_SHIFT | MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;9~"}, + {MOD_SHIFT | MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;9~"}, + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[Z"}, +}; + +static const struct key_data key_backspace[] = { + {MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"}, + {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"}, + {MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"}, + {MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"}, + {MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"}, + {MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"}, + {MOD_META | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"}, + {MOD_META | MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"}, + {MOD_META | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"}, + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x7f"}, + {MOD_META | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"}, + {MOD_META | MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"}, + {MOD_META | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"}, + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE1, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033\x08"}, + + {MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;2;127~"}, + {MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;3;127~"}, + {MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;4;127~"}, + {MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;6;8~"}, + {MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;7;8~"}, + {MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;8;8~"}, + {MOD_META | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;9;127~"}, + {MOD_META | MOD_SHIFT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;10;127~"}, + {MOD_META | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;11;127~"}, + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;12;127~"}, + {MOD_META | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;13;8~"}, + {MOD_META | MOD_SHIFT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;14;8~"}, + {MOD_META | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;15;8~"}, + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL | MOD_MODIFY_OTHER_KEYS_STATE2, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[27;16;8~"}, + + {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x08"}, + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\x7f"}, +}; + +#define DEFAULT_MODS_FOR_SINGLE(sym) \ + {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2"#sym}, \ + {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;3"#sym}, \ + {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;4"#sym}, \ + {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5"#sym}, \ + {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;6"#sym}, \ + {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;7"#sym}, \ + {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;8"#sym}, \ + {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;9"#sym}, \ + {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;10"#sym}, \ + {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;11"#sym}, \ + {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;12"#sym}, \ + {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;13"#sym}, \ + {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;14"#sym}, \ + {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;15"#sym}, \ + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;16"#sym} + +#define DEFAULT_MODS_FOR_TILDE(sym) \ + {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";2~"}, \ + {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";3~"}, \ + {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";4~"}, \ + {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";5~"}, \ + {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";6~"}, \ + {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";7~"}, \ + {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";8~"}, \ + {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";9~"}, \ + {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";10~"}, \ + {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";11~"}, \ + {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";12~"}, \ + {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";13~"}, \ + {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";14~"}, \ + {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";15~"}, \ + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033["#sym";16~"} + + +static const struct key_data key_up[] = { + DEFAULT_MODS_FOR_SINGLE(A), + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OA"}, + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[A"}, + +}; + +static const struct key_data key_down[] = { + DEFAULT_MODS_FOR_SINGLE(B), + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OB"}, + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[B"}, +}; + +static const struct key_data key_right[] = { + DEFAULT_MODS_FOR_SINGLE(C), + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OC"}, + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[C"}, +}; + +static const struct key_data key_left[] = { + DEFAULT_MODS_FOR_SINGLE(D), + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OD"}, + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[D"}, +}; + +static const struct key_data key_home[] = { + DEFAULT_MODS_FOR_SINGLE(H), + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OH"}, + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[H"}, +}; + +static const struct key_data key_end[] = { + DEFAULT_MODS_FOR_SINGLE(F), + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OF"}, + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[F"}, +}; + +static const struct key_data key_insert[] = { + DEFAULT_MODS_FOR_TILDE(2), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[2~"}, +}; + +static const struct key_data key_delete[] = { + DEFAULT_MODS_FOR_TILDE(3), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[3~"}, +}; + +static const struct key_data key_pageup[] = { + DEFAULT_MODS_FOR_TILDE(5), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[5~"}, +}; + +static const struct key_data key_pagedown[] = { + DEFAULT_MODS_FOR_TILDE(6), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[6~"}, +}; + +static const struct key_data key_f1[] = { + DEFAULT_MODS_FOR_SINGLE(P), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OP"}, +}; + +static const struct key_data key_f2[] = { + DEFAULT_MODS_FOR_SINGLE(Q), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OQ"}, +}; + +static const struct key_data key_f3[] = { + DEFAULT_MODS_FOR_SINGLE(R), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OR"}, +}; + +static const struct key_data key_f4[] = { + DEFAULT_MODS_FOR_SINGLE(S), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033OS"}, +}; + +static const struct key_data key_f5[] = { + DEFAULT_MODS_FOR_TILDE(15), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[15~"}, +}; + +static const struct key_data key_f6[] = { + DEFAULT_MODS_FOR_TILDE(17), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[17~"}, +}; +static const struct key_data key_f7[] = { + DEFAULT_MODS_FOR_TILDE(18), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[18~"}, +}; + +static const struct key_data key_f8[] = { + DEFAULT_MODS_FOR_TILDE(19), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[19~"}, +}; + +static const struct key_data key_f9[] = { + DEFAULT_MODS_FOR_TILDE(20), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[20~"}, +}; + +static const struct key_data key_f10[] = { + DEFAULT_MODS_FOR_TILDE(21), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[21~"}, +}; + +static const struct key_data key_f11[] = { + DEFAULT_MODS_FOR_TILDE(23), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[23~"}, +}; + +static const struct key_data key_f12[] = { + DEFAULT_MODS_FOR_TILDE(24), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[24~"}, +}; + +static const struct key_data key_f13[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2P"}}; +static const struct key_data key_f14[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2Q"}}; +static const struct key_data key_f15[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2R"}}; +static const struct key_data key_f16[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;2S"}}; +static const struct key_data key_f17[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[15;2~"}}; +static const struct key_data key_f18[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[17;2~"}}; +static const struct key_data key_f19[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[18;2~"}}; +static const struct key_data key_f20[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[19;2~"}}; +static const struct key_data key_f21[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[20;2~"}}; +static const struct key_data key_f22[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[21;2~"}}; +static const struct key_data key_f23[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[23;2~"}}; +static const struct key_data key_f24[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[24;2~"}}; +static const struct key_data key_f25[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5P"}}; +static const struct key_data key_f26[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5Q"}}; +static const struct key_data key_f27[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5R"}}; +static const struct key_data key_f28[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[1;5S"}}; +static const struct key_data key_f29[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[15;5~"}}; +static const struct key_data key_f30[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[17;5~"}}; +static const struct key_data key_f31[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[18;5~"}}; +static const struct key_data key_f32[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[19;5~"}}; +static const struct key_data key_f33[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[20;5~"}}; +static const struct key_data key_f34[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[21;5~"}}; +static const struct key_data key_f35[] = {{MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[23;5~"}}; + +static const struct key_data key_kp_up[] = { + DEFAULT_MODS_FOR_SINGLE(A), + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[A"}, + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OA"}, +}; + +static const struct key_data key_kp_down[] = { + DEFAULT_MODS_FOR_SINGLE(B), + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[B"}, + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OB"}, +}; + +static const struct key_data key_kp_right[] = { + DEFAULT_MODS_FOR_SINGLE(C), + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[C"}, + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OC"}, +}; + +static const struct key_data key_kp_left[] = { + DEFAULT_MODS_FOR_SINGLE(D), + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[D"}, + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OD"}, +}; + +static const struct key_data key_kp_begin[] = { + DEFAULT_MODS_FOR_SINGLE(E), + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[E"}, + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OE"}, +}; + +static const struct key_data key_kp_home[] = { + DEFAULT_MODS_FOR_SINGLE(H), + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[H"}, + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OH"}, +}; + +static const struct key_data key_kp_end[] = { + DEFAULT_MODS_FOR_SINGLE(F), + {MOD_ANY, CURSOR_KEYS_NORMAL, KEYPAD_DONTCARE, "\033[F"}, + {MOD_ANY, CURSOR_KEYS_APPLICATION, KEYPAD_DONTCARE, "\033OF"}, +}; + +static const struct key_data key_kp_insert[] = { + DEFAULT_MODS_FOR_TILDE(2), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[2~"}, +}; + +static const struct key_data key_kp_delete[] = { + DEFAULT_MODS_FOR_TILDE(3), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[3~"}, +}; + +static const struct key_data key_kp_pageup[] = { + DEFAULT_MODS_FOR_TILDE(5), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[5~"}, +}; + +static const struct key_data key_kp_pagedown[] = { + DEFAULT_MODS_FOR_TILDE(6), + {MOD_ANY, CURSOR_KEYS_DONTCARE, KEYPAD_DONTCARE, "\033[6~"}, +}; + +#undef DEFAULT_MODS_FOR_SINGLE +#undef DEFAULT_MODS_FOR_TILDE + +#define DEFAULT_MODS_FOR_KP(sym) \ + {MOD_NONE, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O"#sym}, \ + {MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O2"#sym}, \ + {MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O3"#sym}, \ + {MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O4"#sym}, \ + {MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O5"#sym}, \ + {MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O6"#sym}, \ + {MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O7"#sym}, \ + {MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O8"#sym}, \ + {MOD_META, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O9"#sym}, \ + {MOD_META | MOD_SHIFT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O10"#sym}, \ + {MOD_META | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O11"#sym}, \ + {MOD_META | MOD_SHIFT | MOD_ALT, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O12"#sym}, \ + {MOD_META | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O13"#sym}, \ + {MOD_META | MOD_SHIFT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O14"#sym}, \ + {MOD_META | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O15"#sym}, \ + {MOD_META | MOD_SHIFT | MOD_ALT | MOD_CTRL, CURSOR_KEYS_DONTCARE, KEYPAD_APPLICATION, "\033O16"#sym} + +static const struct key_data key_kp_enter[] = {DEFAULT_MODS_FOR_KP(M)}; +static const struct key_data key_kp_divide[] = {DEFAULT_MODS_FOR_KP(o)}; +static const struct key_data key_kp_multiply[] = {DEFAULT_MODS_FOR_KP(j)}; +static const struct key_data key_kp_subtract[] = {DEFAULT_MODS_FOR_KP(m)}; +static const struct key_data key_kp_add[] = {DEFAULT_MODS_FOR_KP(k)}; +static const struct key_data key_kp_separator[] = {DEFAULT_MODS_FOR_KP(l)}; +static const struct key_data key_kp_decimal[] = {DEFAULT_MODS_FOR_KP(n)}; +static const struct key_data key_kp_0[] = {DEFAULT_MODS_FOR_KP(p)}; +static const struct key_data key_kp_1[] = {DEFAULT_MODS_FOR_KP(q)}; +static const struct key_data key_kp_2[] = {DEFAULT_MODS_FOR_KP(r)}; +static const struct key_data key_kp_3[] = {DEFAULT_MODS_FOR_KP(s)}; +static const struct key_data key_kp_4[] = {DEFAULT_MODS_FOR_KP(t)}; +static const struct key_data key_kp_5[] = {DEFAULT_MODS_FOR_KP(u)}; +static const struct key_data key_kp_6[] = {DEFAULT_MODS_FOR_KP(v)}; +static const struct key_data key_kp_7[] = {DEFAULT_MODS_FOR_KP(w)}; +static const struct key_data key_kp_8[] = {DEFAULT_MODS_FOR_KP(x)}; +static const struct key_data key_kp_9[] = {DEFAULT_MODS_FOR_KP(y)}; + +#undef DEFAULT_MODS_FOR_KP diff --git a/kitty-keymap.h b/kitty-keymap.h new file mode 100644 index 0000000..3420d01 --- /dev/null +++ b/kitty-keymap.h @@ -0,0 +1,136 @@ +#pragma once + +#include +#include +#include + +struct kitty_key_data { + xkb_keysym_t sym; + uint16_t key; + uint8_t final:7; + bool is_modifier:1; +} __attribute__((packed)); + +_Static_assert(sizeof(struct kitty_key_data) == 7, "bad size"); + +/* Note! *Must* Be kept sorted (on 'sym') */ +static const struct kitty_key_data kitty_keymap[] = { + {XKB_KEY_ISO_Level3_Shift, 57453, 'u', true}, + {XKB_KEY_ISO_Level5_Shift, 57454, 'u', true}, + {XKB_KEY_ISO_Left_Tab, 9, 'u', false}, + + {XKB_KEY_BackSpace, 127, 'u', false}, + {XKB_KEY_Tab, 9, 'u', false}, + {XKB_KEY_Return, 13, 'u', false}, + {XKB_KEY_Pause, 57362, 'u', false}, + {XKB_KEY_Scroll_Lock, 57359, 'u', false}, + {XKB_KEY_Escape, 27, 'u', false}, + {XKB_KEY_Home, 1, 'H', false}, + {XKB_KEY_Left, 1, 'D', false}, + {XKB_KEY_Up, 1, 'A', false}, + {XKB_KEY_Right, 1, 'C', false}, + {XKB_KEY_Down, 1, 'B', false}, + {XKB_KEY_Prior, 5, '~', false}, + {XKB_KEY_Next, 6, '~', false}, + {XKB_KEY_End, 1, 'F', false}, + {XKB_KEY_Print, 57361, 'u', false}, + {XKB_KEY_Insert, 2, '~', false}, + {XKB_KEY_Menu, 57363, 'u', false}, + {XKB_KEY_Num_Lock, 57360, 'u', true}, + + {XKB_KEY_KP_Enter, 57414, 'u', false}, + {XKB_KEY_KP_Home, 57423, 'u', false}, + {XKB_KEY_KP_Left, 57417, 'u', false}, + {XKB_KEY_KP_Up, 57419, 'u', false}, + {XKB_KEY_KP_Right, 57418, 'u', false}, + {XKB_KEY_KP_Down, 57420, 'u', false}, + {XKB_KEY_KP_Prior, 57421, 'u', false}, + {XKB_KEY_KP_Next, 57422, 'u', false}, + {XKB_KEY_KP_End, 57424, 'u', false}, + {XKB_KEY_KP_Begin, 1, 'E', false}, + {XKB_KEY_KP_Insert, 57425, 'u', false}, + {XKB_KEY_KP_Delete, 57426, 'u', false}, + {XKB_KEY_KP_Multiply, 57411, 'u', false}, + {XKB_KEY_KP_Add, 57413, 'u', false}, + {XKB_KEY_KP_Separator, 57416, 'u', false}, + {XKB_KEY_KP_Subtract, 57412, 'u', false}, + {XKB_KEY_KP_Decimal, 57409, 'u', false}, + {XKB_KEY_KP_Divide, 57410, 'u', false}, + {XKB_KEY_KP_0, 57399, 'u', false}, + {XKB_KEY_KP_1, 57400, 'u', false}, + {XKB_KEY_KP_2, 57401, 'u', false}, + {XKB_KEY_KP_3, 57402, 'u', false}, + {XKB_KEY_KP_4, 57403, 'u', false}, + {XKB_KEY_KP_5, 57404, 'u', false}, + {XKB_KEY_KP_6, 57405, 'u', false}, + {XKB_KEY_KP_7, 57406, 'u', false}, + {XKB_KEY_KP_8, 57407, 'u', false}, + {XKB_KEY_KP_9, 57408, 'u', false}, + {XKB_KEY_KP_Equal, 57415, 'u', false}, + + {XKB_KEY_F1, 1, 'P', false}, + {XKB_KEY_F2, 1, 'Q', false}, + {XKB_KEY_F3, 13, '~', false}, + {XKB_KEY_F4, 1, 'S', false}, + {XKB_KEY_F5, 15, '~', false}, + {XKB_KEY_F6, 17, '~', false}, + {XKB_KEY_F7, 18, '~', false}, + {XKB_KEY_F8, 19, '~', false}, + {XKB_KEY_F9, 20, '~', false}, + {XKB_KEY_F10, 21, '~', false}, + {XKB_KEY_F11, 23, '~', false}, + {XKB_KEY_F12, 24, '~', false}, + {XKB_KEY_F13, 57376, 'u', false}, + {XKB_KEY_F14, 57377, 'u', false}, + {XKB_KEY_F15, 57378, 'u', false}, + {XKB_KEY_F16, 57379, 'u', false}, + {XKB_KEY_F17, 57380, 'u', false}, + {XKB_KEY_F18, 57381, 'u', false}, + {XKB_KEY_F19, 57382, 'u', false}, + {XKB_KEY_F20, 57383, 'u', false}, + {XKB_KEY_F21, 57384, 'u', false}, + {XKB_KEY_F22, 57385, 'u', false}, + {XKB_KEY_F23, 57386, 'u', false}, + {XKB_KEY_F24, 57387, 'u', false}, + {XKB_KEY_F25, 57388, 'u', false}, + {XKB_KEY_F26, 57389, 'u', false}, + {XKB_KEY_F27, 57390, 'u', false}, + {XKB_KEY_F28, 57391, 'u', false}, + {XKB_KEY_F29, 57392, 'u', false}, + {XKB_KEY_F30, 57393, 'u', false}, + {XKB_KEY_F31, 57394, 'u', false}, + {XKB_KEY_F32, 57395, 'u', false}, + {XKB_KEY_F33, 57396, 'u', false}, + {XKB_KEY_F34, 57397, 'u', false}, + {XKB_KEY_F35, 57398, 'u', false}, + + {XKB_KEY_Shift_L, 57441, 'u', true}, + {XKB_KEY_Shift_R, 57447, 'u', true}, + {XKB_KEY_Control_L, 57442, 'u', true}, + {XKB_KEY_Control_R, 57448, 'u', true}, + {XKB_KEY_Caps_Lock, 57358, 'u', true}, + {XKB_KEY_Meta_L, 57446, 'u', true}, + {XKB_KEY_Meta_R, 57452, 'u', true}, + {XKB_KEY_Alt_L, 57443, 'u', true}, + {XKB_KEY_Alt_R, 57449, 'u', true}, + {XKB_KEY_Super_L, 57444, 'u', true}, + {XKB_KEY_Super_R, 57450, 'u', true}, + {XKB_KEY_Hyper_L, 57445, 'u', true}, + {XKB_KEY_Hyper_R, 57451, 'u', true}, + + {XKB_KEY_Delete, 3, '~', false}, + + {XKB_KEY_XF86AudioLowerVolume, 57438, 'u', false}, + {XKB_KEY_XF86AudioMute, 57440, 'u', false}, + {XKB_KEY_XF86AudioRaiseVolume, 57439, 'u', false}, + {XKB_KEY_XF86AudioPlay, 57428, 'u', false}, + {XKB_KEY_XF86AudioStop, 57432, 'u', false}, + {XKB_KEY_XF86AudioPrev, 57436, 'u', false}, + {XKB_KEY_XF86AudioNext, 57435, 'u', false}, + {XKB_KEY_XF86AudioRecord, 57437, 'u', false}, + {XKB_KEY_XF86AudioPause, 57429, 'u', false}, + {XKB_KEY_XF86AudioRewind, 57434, 'u', false}, + {XKB_KEY_XF86AudioForward, 57433, 'u', false}, + //{XKB_KEY_XF86AudioPlayPause, 57430, 'u', false}, + //{XKB_KEY_XF86AudioReverse, 57431, 'u', false}, +}; diff --git a/log.c b/log.c new file mode 100644 index 0000000..ebf411e --- /dev/null +++ b/log.c @@ -0,0 +1,231 @@ +#include "log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "debug.h" +#include "util.h" +#include "xsnprintf.h" + +static bool colorize = false; +static bool do_syslog = false; +static enum log_class log_level = LOG_CLASS_NONE; + +static const struct { + const char name[8]; + const char log_prefix[7]; + uint8_t color; + int syslog_equivalent; +} log_level_map[] = { + [LOG_CLASS_NONE] = {"none", "none", 5, -1}, + [LOG_CLASS_ERROR] = {"error", " err", 31, LOG_ERR}, + [LOG_CLASS_WARNING] = {"warning", "warn", 33, LOG_WARNING}, + [LOG_CLASS_INFO] = {"info", "info", 97, LOG_INFO}, + [LOG_CLASS_DEBUG] = {"debug", " dbg", 36, LOG_DEBUG}, +}; + +void +log_init(enum log_colorize _colorize, bool _do_syslog, + enum log_facility syslog_facility, enum log_class _log_level) +{ + static const int facility_map[] = { + [LOG_FACILITY_USER] = LOG_USER, + [LOG_FACILITY_DAEMON] = LOG_DAEMON, + }; + + /* Don't use colors if NO_COLOR is defined and not empty */ + const char *no_color_str = getenv("NO_COLOR"); + const bool no_color = no_color_str != NULL && no_color_str[0] != '\0'; + + colorize = _colorize == LOG_COLORIZE_ALWAYS + || (_colorize == LOG_COLORIZE_AUTO + && !no_color && isatty(STDERR_FILENO)); + do_syslog = _do_syslog; + log_level = _log_level; + + int slvl = log_level_map[_log_level].syslog_equivalent; + if (slvl < 0) + do_syslog = false; + + if (do_syslog) { + openlog(NULL, /*LOG_PID*/0, facility_map[syslog_facility]); + + xassert(slvl >= 0); + setlogmask(LOG_UPTO(slvl)); + } +} + +void +log_deinit(void) +{ + if (do_syslog) + closelog(); +} + +static void +_log(enum log_class log_class, const char *module, const char *file, int lineno, + const char *fmt, int sys_errno, va_list va) +{ + xassert(log_class > LOG_CLASS_NONE); + xassert(log_class < ALEN(log_level_map)); + + if (log_class > log_level) + return; + + const char *prefix = log_level_map[log_class].log_prefix; + unsigned int class_clr = log_level_map[log_class].color; + + char clr[16]; + xsnprintf(clr, sizeof(clr), "\033[%um", class_clr); + fprintf(stderr, "%s%s%s: ", colorize ? clr : "", prefix, colorize ? "\033[0m" : ""); + + if (colorize) + fputs("\033[2m", stderr); + fprintf(stderr, "%s:%d: ", file, lineno); + if (colorize) + fputs("\033[0m", stderr); + + vfprintf(stderr, fmt, va); + + if (sys_errno != 0) + fprintf(stderr, ": %s", strerror(sys_errno)); + + fputc('\n', stderr); +} + +static void +_sys_log(enum log_class log_class, const char *module, + const char UNUSED *file, int UNUSED lineno, + const char *fmt, int sys_errno, va_list va) +{ + xassert(log_class > LOG_CLASS_NONE); + xassert(log_class < ALEN(log_level_map)); + + if (!do_syslog) + return; + + if (log_class > log_level) + return; + + /* Map our log level to syslog's level */ + int level = log_level_map[log_class].syslog_equivalent; + + char msg[4096]; + int n = vsnprintf(msg, sizeof(msg), fmt, va); + xassert(n >= 0); + + if (sys_errno != 0 && (size_t)n < sizeof(msg)) + snprintf(msg + n, sizeof(msg) - n, ": %s", strerror(sys_errno)); + + syslog(level, "%s: %s", module, msg); +} + +void +log_msg_va(enum log_class log_class, const char *module, + const char *file, int lineno, const char *fmt, va_list va) +{ + va_list va2; + va_copy(va2, va); + _log(log_class, module, file, lineno, fmt, 0, va); + _sys_log(log_class, module, file, lineno, fmt, 0, va2); + va_end(va2); +} + +void +log_msg(enum log_class log_class, const char *module, + const char *file, int lineno, const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + log_msg_va(log_class, module, file, lineno, fmt, va); + va_end(va); +} + +void +log_errno_va(enum log_class log_class, const char *module, + const char *file, int lineno, + const char *fmt, va_list va) +{ + log_errno_provided_va(log_class, module, file, lineno, errno, fmt, va); +} + +void +log_errno(enum log_class log_class, const char *module, + const char *file, int lineno, + const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + log_errno_va(log_class, module, file, lineno, fmt, va); + va_end(va); +} + +void +log_errno_provided_va(enum log_class log_class, const char *module, + const char *file, int lineno, int errno_copy, + const char *fmt, va_list va) +{ + va_list va2; + va_copy(va2, va); + _log(log_class, module, file, lineno, fmt, errno_copy, va); + _sys_log(log_class, module, file, lineno, fmt, errno_copy, va2); + va_end(va2); +} + +void +log_errno_provided(enum log_class log_class, const char *module, + const char *file, int lineno, int errno_copy, + const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + log_errno_provided_va(log_class, module, file, lineno, errno_copy, fmt, va); + va_end(va); +} + +static size_t +map_len(void) +{ + size_t len = ALEN(log_level_map); +#ifndef _DEBUG + /* Exclude "debug" entry for non-debug builds */ + len--; +#endif + return len; +} + +int +log_level_from_string(const char *str) +{ + if (unlikely(str[0] == '\0')) + return -1; + + for (int i = 0, n = map_len(); i < n; i++) + if (streq(str, log_level_map[i].name)) + return i; + + return -1; +} + +const char * +log_level_string_hint(void) +{ + static char buf[64]; + if (buf[0] != '\0') + return buf; + + for (size_t i = 0, pos = 0, n = map_len(); i < n; i++) { + const char *entry = log_level_map[i].name; + const char *delim = (i + 1 < n) ? ", " : ""; + pos += xsnprintf(buf + pos, sizeof(buf) - pos, "'%s'%s", entry, delim); + } + + return buf; +} diff --git a/log.h b/log.h new file mode 100644 index 0000000..e499cf2 --- /dev/null +++ b/log.h @@ -0,0 +1,70 @@ +#pragma once +#include +#include +#include "macros.h" + +enum log_colorize { LOG_COLORIZE_NEVER, LOG_COLORIZE_ALWAYS, LOG_COLORIZE_AUTO }; +enum log_facility { LOG_FACILITY_USER, LOG_FACILITY_DAEMON }; + +enum log_class { + LOG_CLASS_NONE, + LOG_CLASS_ERROR, + LOG_CLASS_WARNING, + LOG_CLASS_INFO, + LOG_CLASS_DEBUG, + LOG_CLASS_COUNT, +}; + +void log_init(enum log_colorize colorize, bool do_syslog, + enum log_facility syslog_facility, enum log_class log_level); +void log_deinit(void); + +void log_msg( + enum log_class log_class, const char *module, + const char *file, int lineno, + const char *fmt, ...) PRINTF(5); + +void log_errno( + enum log_class log_class, const char *module, + const char *file, int lineno, + const char *fmt, ...) PRINTF(5); + +void log_errno_provided( + enum log_class log_class, const char *module, + const char *file, int lineno, int _errno, + const char *fmt, ...) PRINTF(6); + +void log_msg_va( + enum log_class log_class, const char *module, + const char *file, int lineno, const char *fmt, va_list va) VPRINTF(5); +void log_errno_va( + enum log_class log_class, const char *module, + const char *file, int lineno, + const char *fmt, va_list va) VPRINTF(5); +void log_errno_provided_va( + enum log_class log_class, const char *module, + const char *file, int lineno, int _errno, + const char *fmt, va_list va) VPRINTF(6); + + +int log_level_from_string(const char *str); +const char *log_level_string_hint(void); + +#define LOG_ERR(...) \ + log_msg(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) +#define LOG_ERRNO(...) \ + log_errno(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) +#define LOG_ERRNO_P(_errno, ...) \ + log_errno_provided(LOG_CLASS_ERROR, LOG_MODULE, __FILE__, __LINE__, \ + _errno, __VA_ARGS__) +#define LOG_WARN(...) \ + log_msg(LOG_CLASS_WARNING, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) +#define LOG_INFO(...) \ + log_msg(LOG_CLASS_INFO, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + #define LOG_DBG(...) \ + log_msg(LOG_CLASS_DEBUG, LOG_MODULE, __FILE__, __LINE__, __VA_ARGS__) +#else + #define LOG_DBG(...) +#endif diff --git a/macros.h b/macros.h new file mode 100644 index 0000000..2ecda60 --- /dev/null +++ b/macros.h @@ -0,0 +1,211 @@ +#pragma once + +#define PASTE(a, b) a##b +#define XPASTE(a, b) PASTE(a, b) +#define STRLEN(str) (sizeof("" str "") - 1) +#define DO_PRAGMA(x) _Pragma(#x) +#define VERCMP(x, y, cx, cy) ((cx > x) || ((cx == x) && (cy >= y))) + +#if defined(__GNUC__) && defined(__GNUC_MINOR__) + #define GNUC_AT_LEAST(x, y) VERCMP(x, y, __GNUC__, __GNUC_MINOR__) +#else + #define GNUC_AT_LEAST(x, y) 0 +#endif + +#if defined(__clang_major__) && defined(__clang_minor__) + #define CLANG_AT_LEAST(x, y) VERCMP(x, y, __clang_major__, __clang_minor__) +#else + #define CLANG_AT_LEAST(x, y) 0 +#endif + +#ifdef __has_attribute + #define HAS_ATTRIBUTE(x) __has_attribute(x) +#else + #define HAS_ATTRIBUTE(x) 0 +#endif + +#ifdef __has_builtin + #define HAS_BUILTIN(x) __has_builtin(x) +#else + #define HAS_BUILTIN(x) 0 +#endif + +#ifdef __has_include + #define HAS_INCLUDE(x) __has_include(x) +#else + #define HAS_INCLUDE(x) 0 +#endif + +#ifdef __has_feature + #define HAS_FEATURE(x) __has_feature(x) +#else + #define HAS_FEATURE(x) 0 +#endif + +// __has_extension() is a Clang macro used to determine if a feature is +// available even if not standardized in the current "-std" mode. +#ifdef __has_extension + #define HAS_EXTENSION(x) __has_extension(x) +#else + // Clang versions prior to 3.0 only supported __has_feature() + #define HAS_EXTENSION(x) HAS_FEATURE(x) +#endif + +#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(unused) || defined(__TINYC__) + #define UNUSED __attribute__((__unused__)) +#else + #define UNUSED +#endif + +#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(const) + #define CONST __attribute__((__const__)) +#else + #define CONST +#endif + +#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(malloc) + #define MALLOC __attribute__((__malloc__)) +#else + #define MALLOC +#endif + +#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(constructor) + #define CONSTRUCTOR __attribute__((__constructor__)) + #define HAVE_ATTR_CONSTRUCTOR 1 +#else + #define CONSTRUCTOR +#endif + +#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(destructor) + #define DESTRUCTOR __attribute__((__destructor__)) +#else + #define DESTRUCTOR +#endif + +#if GNUC_AT_LEAST(3, 0) || HAS_ATTRIBUTE(format) + #define PRINTF(x) __attribute__((__format__(__printf__, (x), (x + 1)))) + #define VPRINTF(x) __attribute__((__format__(__printf__, (x), 0))) +#else + #define PRINTF(x) + #define VPRINTF(x) +#endif + +#if (GNUC_AT_LEAST(3, 0) || HAS_BUILTIN(__builtin_expect)) && defined(__OPTIMIZE__) + #define likely(x) __builtin_expect(!!(x), 1) + #define unlikely(x) __builtin_expect(!!(x), 0) +#else + #define likely(x) (x) + #define unlikely(x) (x) +#endif + +#if GNUC_AT_LEAST(3, 1) || HAS_ATTRIBUTE(noinline) + #define NOINLINE __attribute__((__noinline__)) +#else + #define NOINLINE +#endif + +#if GNUC_AT_LEAST(3, 1) || HAS_ATTRIBUTE(always_inline) + #define ALWAYS_INLINE __attribute__((__always_inline__)) +#else + #define ALWAYS_INLINE +#endif + +#if GNUC_AT_LEAST(3, 3) || HAS_ATTRIBUTE(nonnull) + #define NONNULL_ARGS __attribute__((__nonnull__)) + #define NONNULL_ARG(...) __attribute__((__nonnull__(__VA_ARGS__))) +#else + #define NONNULL_ARGS + #define NONNULL_ARG(...) +#endif + +#if GNUC_AT_LEAST(3, 4) || HAS_ATTRIBUTE(warn_unused_result) + #define WARN_UNUSED_RESULT __attribute__((__warn_unused_result__)) +#else + #define WARN_UNUSED_RESULT +#endif + +#if GNUC_AT_LEAST(4, 1) || HAS_ATTRIBUTE(flatten) + #define FLATTEN __attribute__((__flatten__)) +#else + #define FLATTEN +#endif + +#if GNUC_AT_LEAST(4, 3) || HAS_ATTRIBUTE(hot) + #define HOT __attribute__((__hot__)) +#else + #define HOT +#endif + +#if GNUC_AT_LEAST(4, 3) || HAS_ATTRIBUTE(cold) + #define COLD __attribute__((__cold__)) +#else + #define COLD +#endif + +#if GNUC_AT_LEAST(4, 5) || HAS_BUILTIN(__builtin_unreachable) + #define UNREACHABLE() __builtin_unreachable() +#else + #define UNREACHABLE() +#endif + +#if GNUC_AT_LEAST(5, 0) || HAS_ATTRIBUTE(returns_nonnull) + #define RETURNS_NONNULL __attribute__((__returns_nonnull__)) +#else + #define RETURNS_NONNULL +#endif + +#if HAS_ATTRIBUTE(diagnose_if) + #define DIAGNOSE_IF(x) __attribute__((diagnose_if((x), (#x), "error"))) +#else + #define DIAGNOSE_IF(x) +#endif + +#define XMALLOC MALLOC RETURNS_NONNULL WARN_UNUSED_RESULT +#define XSTRDUP XMALLOC NONNULL_ARGS + +#if __STDC_VERSION__ >= 201112L + #define noreturn _Noreturn +#elif GNUC_AT_LEAST(3, 0) + #define noreturn __attribute__((__noreturn__)) +#else + #define noreturn +#endif + +#if CLANG_AT_LEAST(3, 6) + #define UNROLL_LOOP(n) DO_PRAGMA(clang loop unroll_count(n)) +#elif GNUC_AT_LEAST(8, 0) + #define UNROLL_LOOP(n) DO_PRAGMA(GCC unroll (n)) +#else + #define UNROLL_LOOP(n) +#endif + +#ifdef __COUNTER__ + // Supported by GCC 4.3+ and Clang + #define COUNTER_ __COUNTER__ +#else + #define COUNTER_ __LINE__ +#endif + +#if defined(_DEBUG) && defined(HAVE_ATTR_CONSTRUCTOR) + #define UNITTEST static void CONSTRUCTOR XPASTE(unittest_, COUNTER_)(void) +#else + #define UNITTEST static void UNUSED XPASTE(unittest_, COUNTER_)(void) +#endif + +#ifdef __clang__ + #define IGNORE_WARNING(wflag) \ + DO_PRAGMA(clang diagnostic push) \ + DO_PRAGMA(clang diagnostic ignored "-Wunknown-pragmas") \ + DO_PRAGMA(clang diagnostic ignored "-Wunknown-warning-option") \ + DO_PRAGMA(clang diagnostic ignored wflag) + #define UNIGNORE_WARNINGS DO_PRAGMA(clang diagnostic pop) +#elif GNUC_AT_LEAST(4, 6) + #define IGNORE_WARNING(wflag) \ + DO_PRAGMA(GCC diagnostic push) \ + DO_PRAGMA(GCC diagnostic ignored "-Wpragmas") \ + DO_PRAGMA(GCC diagnostic ignored wflag) + #define UNIGNORE_WARNINGS DO_PRAGMA(GCC diagnostic pop) +#else + #define IGNORE_WARNING(wflag) + #define UNIGNORE_WARNINGS +#endif diff --git a/main.c b/main.c new file mode 100644 index 0000000..9db77d0 --- /dev/null +++ b/main.c @@ -0,0 +1,726 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#define LOG_MODULE "main" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "config.h" +#include "fdm.h" +#include "foot-features.h" +#include "key-binding.h" +#include "macros.h" +#include "reaper.h" +#include "render.h" +#include "server.h" +#include "shm.h" +#include "terminal.h" +#include "util.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +#if !defined(__STDC_UTF_32__) || !__STDC_UTF_32__ + #error "char32_t does not use UTF-32" +#endif + +static bool +fdm_sigint(struct fdm *fdm, int signo, void *data) +{ + *(volatile sig_atomic_t *)data = true; + return true; +} + +struct sigusr_context { + struct terminal *term; + struct server *server; +}; + +static bool +fdm_sigusr(struct fdm *fdm, int signo, void *data) +{ + xassert(signo == SIGUSR1 || signo == SIGUSR2); + + struct sigusr_context *ctx = data; + + if (ctx->server != NULL) { + if (signo == SIGUSR1) + server_global_theme_switch_to_dark(ctx->server); + else + server_global_theme_switch_to_light(ctx->server); + } else { + if (signo == SIGUSR1) + term_theme_switch_to_dark(ctx->term); + else + term_theme_switch_to_light(ctx->term); + } + + return true; +} + +static void +print_usage(const char *prog_name) +{ + static const char options[] = + "\nOptions:\n" + " -c,--config=PATH load configuration from PATH ($XDG_CONFIG_HOME/foot/foot.ini)\n" + " -C,--check-config verify configuration, exit with 0 if ok, otherwise exit with 1\n" + " -o,--override=[section.]key=value override configuration option\n" + " -f,--font=FONT comma separated list of fonts in fontconfig format (monospace)\n" + " -t,--term=TERM value to set the environment variable TERM to (" FOOT_DEFAULT_TERM ")\n" + " -T,--title=TITLE initial window title (foot)\n" + " -a,--app-id=ID window application ID (foot)\n" + " --toplevel-tag=TAG set a custom toplevel tag\n" + " -m,--maximized start in maximized mode\n" + " -F,--fullscreen start in fullscreen mode\n" + " -L,--login-shell start shell as a login shell\n" + " --pty=PATH display an existing PTY instead of creating one\n" + " -D,--working-directory=DIR directory to start in (CWD)\n" + " -w,--window-size-pixels=WIDTHxHEIGHT initial width and height, in pixels\n" + " -W,--window-size-chars=WIDTHxHEIGHT initial width and height, in characters\n" + " -s,--server[=PATH] run as a server (use 'footclient' to start terminals).\n" + " Without PATH, $XDG_RUNTIME_DIR/foot-$WAYLAND_DISPLAY.sock will be used.\n" + " -H,--hold remain open after child process exits\n" + " -p,--print-pid=FILE|FD print PID to file or FD (only applicable in server mode)\n" + " -d,--log-level={info|warning|error|none} log level (warning)\n" + " -l,--log-colorize=[{never|always|auto}] enable/disable colorization of log output on stderr\n" + " -S,--log-no-syslog disable syslog logging (only applicable in server mode)\n" + " -v,--version show the version number and quit\n" + " -e ignored (for compatibility with xterm -e)\n"; + + printf("Usage: %s [OPTIONS...]\n", prog_name); + printf("Usage: %s [OPTIONS...] command [ARGS...]\n", prog_name); + puts(options); +} + +bool +locale_is_utf8(void) +{ + static const char u8[] = u8"ö"; + xassert(strlen(u8) == 2); + + char32_t w; + if (mbrtoc32(&w, u8, 2, &(mbstate_t){0}) != 2) + return false; + + return w == U'ö'; +} + +struct shutdown_context { + struct terminal **term; + int exit_code; +}; + +static void +term_shutdown_cb(void *data, int exit_code) +{ + struct shutdown_context *ctx = data; + *ctx->term = NULL; + ctx->exit_code = exit_code; +} + +static bool +print_pid(const char *pid_file, bool *unlink_at_exit) +{ + LOG_DBG("printing PID to %s", pid_file); + + errno = 0; + char *end; + int pid_fd = strtoul(pid_file, &end, 10); + + if (errno != 0 || *end != '\0') { + if ((pid_fd = open(pid_file, + O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, + S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) < 0) { + LOG_ERRNO("%s: failed to open", pid_file); + return false; + } else + *unlink_at_exit = true; + } + + if (pid_fd >= 0) { + char pid[32]; + size_t n = xsnprintf(pid, sizeof(pid), "%u\n", getpid()); + + ssize_t bytes = write(pid_fd, pid, n); + close(pid_fd); + + if (bytes < 0) { + LOG_ERRNO("failed to write PID to FD=%u", pid_fd); + return false; + } + + LOG_DBG("wrote %zd bytes to FD=%d", bytes, pid_fd); + return true; + } else + return false; +} + +static void +sanitize_signals(void) +{ + sigset_t mask; + sigemptyset(&mask); + sigprocmask(SIG_SETMASK, &mask, NULL); + + struct sigaction dfl = {.sa_handler = SIG_DFL}; + sigemptyset(&dfl.sa_mask); + + for (int i = 1; i < SIGRTMAX; i++) + sigaction(i, &dfl, NULL); +} + +enum { + PTY_OPTION = CHAR_MAX + 1, + TOPLEVEL_TAG_OPTION = CHAR_MAX + 2, +}; + +int +main(int argc, char *const *argv) +{ + /* Custom exit code, to enable users to differentiate between foot + * itself failing, and the client application failing */ + static const int foot_exit_failure = -26; + int ret = foot_exit_failure; + + sanitize_signals(); + + /* XDG startup notifications */ + const char *token = getenv("XDG_ACTIVATION_TOKEN"); + unsetenv("XDG_ACTIVATION_TOKEN"); + + /* Startup notifications; we don't support it, but must ensure we + * don't pass this on to programs launched by us */ + unsetenv("DESKTOP_STARTUP_ID"); + + const char *const prog_name = argc > 0 ? argv[0] : ""; + + static const struct option longopts[] = { + {"config", required_argument, NULL, 'c'}, + {"check-config", no_argument, NULL, 'C'}, + {"override", required_argument, NULL, 'o'}, + {"term", required_argument, NULL, 't'}, + {"title", required_argument, NULL, 'T'}, + {"app-id", required_argument, NULL, 'a'}, + {"toplevel-tag", required_argument, NULL, TOPLEVEL_TAG_OPTION}, + {"login-shell", no_argument, NULL, 'L'}, + {"working-directory", required_argument, NULL, 'D'}, + {"font", required_argument, NULL, 'f'}, + {"window-size-pixels", required_argument, NULL, 'w'}, + {"window-size-chars", required_argument, NULL, 'W'}, + {"server", optional_argument, NULL, 's'}, + {"hold", no_argument, NULL, 'H'}, + {"maximized", no_argument, NULL, 'm'}, + {"fullscreen", no_argument, NULL, 'F'}, + {"presentation-timings", no_argument, NULL, 'P'}, /* Undocumented */ + {"pty", required_argument, NULL, PTY_OPTION}, + {"print-pid", required_argument, NULL, 'p'}, + {"log-level", required_argument, NULL, 'd'}, + {"log-colorize", optional_argument, NULL, 'l'}, + {"log-no-syslog", no_argument, NULL, 'S'}, + {"version", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, + {NULL, no_argument, NULL, 0}, + }; + + bool check_config = false; + const char *conf_path = NULL; + const char *custom_cwd = NULL; + const char *pty_path = NULL; + bool as_server = false; + const char *conf_server_socket_path = NULL; + bool presentation_timings = false; + bool hold = false; + bool unlink_pid_file = false; + const char *pid_file = NULL; + enum log_class log_level = LOG_CLASS_WARNING; + enum log_colorize log_colorize = LOG_COLORIZE_AUTO; + bool log_syslog = true; + user_notifications_t user_notifications = tll_init(); + config_override_t overrides = tll_init(); + + while (true) { + int c = getopt_long(argc, argv, "+c:Co:t:T:a:LD:f:w:W:s::HmFPp:d:l::Sveh", longopts, NULL); + + if (c == -1) + break; + + switch (c) { + case 'c': + conf_path = optarg; + break; + + case 'C': + check_config = true; + break; + + case 'o': + tll_push_back(overrides, xstrdup(optarg)); + break; + + case 't': + tll_push_back(overrides, xstrjoin("term=", optarg)); + break; + + case 'L': + tll_push_back(overrides, xstrdup("login-shell=yes")); + break; + + case 'T': + tll_push_back(overrides, xstrjoin("title=", optarg)); + break; + + case 'a': + tll_push_back(overrides, xstrjoin("app-id=", optarg)); + break; + + case TOPLEVEL_TAG_OPTION: + tll_push_back(overrides, xstrjoin("toplevel-tag=", optarg)); + break; + + case 'D': { + struct stat st; + if (stat(optarg, &st) < 0 || !(st.st_mode & S_IFDIR)) { + fprintf(stderr, "error: %s: not a directory\n", optarg); + return ret; + } + custom_cwd = optarg; + break; + } + + case 'f': { + char *font_override = xstrjoin("font=", optarg); + tll_push_back(overrides, font_override); + break; + } + + case 'w': { + unsigned width, height; + if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) { + fprintf(stderr, "error: invalid window-size-pixels: %s\n", optarg); + return ret; + } + + tll_push_back( + overrides, xasprintf("initial-window-size-pixels=%ux%u", + width, height)); + break; + } + + case 'W': { + unsigned width, height; + if (sscanf(optarg, "%ux%u", &width, &height) != 2 || width == 0 || height == 0) { + fprintf(stderr, "error: invalid window-size-chars: %s\n", optarg); + return ret; + } + + tll_push_back( + overrides, xasprintf("initial-window-size-chars=%ux%u", + width, height)); + break; + } + + case 's': + as_server = true; + if (optarg != NULL) + conf_server_socket_path = optarg; + break; + + case PTY_OPTION: + pty_path = optarg; + break; + + case 'P': + presentation_timings = true; + break; + + case 'H': + hold = true; + break; + + case 'm': + tll_push_back(overrides, xstrdup("initial-window-mode=maximized")); + break; + + case 'F': + tll_push_back(overrides, xstrdup("initial-window-mode=fullscreen")); + break; + + case 'p': + pid_file = optarg; + break; + + case 'd': { + int lvl = log_level_from_string(optarg); + if (unlikely(lvl < 0)) { + fprintf( + stderr, + "-d,--log-level: %s: argument must be one of %s\n", + optarg, + log_level_string_hint()); + return ret; + } + log_level = lvl; + break; + } + + case 'l': + if (optarg == NULL || streq(optarg, "auto")) + log_colorize = LOG_COLORIZE_AUTO; + else if (streq(optarg, "never")) + log_colorize = LOG_COLORIZE_NEVER; + else if (streq(optarg, "always")) + log_colorize = LOG_COLORIZE_ALWAYS; + else { + fprintf(stderr, "%s: argument must be one of 'never', 'always' or 'auto'\n", optarg); + return ret; + } + break; + + case 'S': + log_syslog = false; + break; + + case 'v': + print_version_and_features("foot "); + return EXIT_SUCCESS; + + case 'h': + print_usage(prog_name); + return EXIT_SUCCESS; + + case 'e': + break; + + case '?': + return ret; + } + } + + if (as_server && pty_path) { + fputs("error: --pty is incompatible with server mode\n", stderr); + return ret; + } + + log_init(log_colorize, as_server && log_syslog, + as_server ? LOG_FACILITY_DAEMON : LOG_FACILITY_USER, log_level); + + if (argc > 0) { + argc -= optind; + argv += optind; + } + + LOG_INFO("%s", version_and_features); + + { + struct utsname name; + if (uname(&name) < 0) + LOG_ERRNO("uname() failed"); + else + LOG_INFO("arch: %s %s/%zu-bit", + name.sysname, name.machine, sizeof(void *) * 8); + } + + srand(time(NULL)); + + const char *locale = setlocale(LC_CTYPE, ""); + if (locale == NULL) { + /* + * If the user has configured an invalid locale, or a name of a locale + * that does not exist on this system, then the above call may return + * NULL. We should just continue with the fallback method below. + */ + LOG_ERR("setlocale() failed. The most common cause is that the " + "configured locale is not available, or has been misspelled"); + } + + LOG_INFO("locale: %s", locale != NULL ? locale : ""); + + bool bad_locale = locale == NULL || !locale_is_utf8(); + if (bad_locale) { + static const char fallback_locales[][12] = { + "C.UTF-8", + "en_US.UTF-8", + }; + char *saved_locale = locale != NULL ? xstrdup(locale) : NULL; + + /* + * Try to force an UTF-8 locale. If we succeed, launch the + * user's shell as usual, but add a user-notification saying + * the locale has been changed. + */ + for (size_t i = 0; i < ALEN(fallback_locales); i++) { + const char *const fallback_locale = fallback_locales[i]; + + if (setlocale(LC_CTYPE, fallback_locale) != NULL) { + if (saved_locale != NULL) { + LOG_WARN( + "'%s' is not a UTF-8 locale, falling back to '%s'", + saved_locale, fallback_locale); + + user_notification_add_fmt( + &user_notifications, USER_NOTIFICATION_WARNING, + "'%s' is not a UTF-8 locale, falling back to '%s'", + saved_locale, fallback_locale); + + } else { + LOG_WARN( + "invalid locale, falling back to '%s'", fallback_locale); + user_notification_add_fmt( + &user_notifications, USER_NOTIFICATION_WARNING, + "invalid locale, falling back to '%s'", fallback_locale); + } + + bad_locale = false; + break; + } + } + + if (bad_locale) { + if (saved_locale != NULL) { + LOG_ERR( + "'%s' is not a UTF-8 locale, and failed to find a fallback", + saved_locale); + + user_notification_add_fmt( + &user_notifications, USER_NOTIFICATION_ERROR, + "'%s' is not a UTF-8 locale, and failed to find a fallback", + saved_locale); + } else { + LOG_ERR("invalid locale, and failed to find a fallback"); + + user_notification_add_fmt( + &user_notifications, USER_NOTIFICATION_ERROR, + "invalid locale, and failed to find a fallback"); + } + } + free(saved_locale); + } + + struct config conf = {NULL}; + bool conf_successful = config_load( + &conf, conf_path, &user_notifications, &overrides, check_config, as_server); + + tll_free_and_free(overrides, free); + if (!conf_successful) { + config_free(&conf); + return ret; + } + + if (check_config) { + config_free(&conf); + return EXIT_SUCCESS; + } + + _Static_assert((int)LOG_CLASS_ERROR == (int)FCFT_LOG_CLASS_ERROR, + "fcft log level enum offset"); + _Static_assert((int)LOG_COLORIZE_ALWAYS == (int)FCFT_LOG_COLORIZE_ALWAYS, + "fcft colorize enum mismatch"); + fcft_init( + (enum fcft_log_colorize)log_colorize, + as_server && log_syslog, + (enum fcft_log_class)log_level); + + if (conf_server_socket_path != NULL) { + free(conf.server_socket_path); + conf.server_socket_path = xstrdup(conf_server_socket_path); + } + conf.presentation_timings = presentation_timings; + conf.hold_at_exit = hold; + + if (conf.tweak.font_monospace_warn && conf.fonts[0].count > 0) { + check_if_font_is_monospaced( + conf.fonts[0].arr[0].pattern, &conf.notifications); + } + + + if (bad_locale) { + static char *const bad_locale_fake_argv[] = {"/bin/sh", "-c", "", NULL}; + argc = 1; + argv = bad_locale_fake_argv; + conf.hold_at_exit = true; + } + + struct fdm *fdm = NULL; + struct reaper *reaper = NULL; + struct key_binding_manager *key_binding_manager = NULL; + struct wayland *wayl = NULL; + struct renderer *renderer = NULL; + struct terminal *term = NULL; + struct server *server = NULL; + struct shutdown_context shutdown_ctx = {.term = &term, .exit_code = foot_exit_failure}; + + const char *cwd = custom_cwd; + char *_cwd = NULL; + + if (cwd == NULL) { + size_t buf_len = 1024; + do { + _cwd = xrealloc(_cwd, buf_len); + errno = 0; + if (getcwd(_cwd, buf_len) == NULL && errno != ERANGE) { + LOG_ERRNO("failed to get current working directory"); + goto out; + } + buf_len *= 2; + } while (errno == ERANGE); + cwd = _cwd; + } + + const char *pwd = getenv("PWD"); + if (pwd != NULL) { + char *resolved_path_cwd = realpath(cwd, NULL); + char *resolved_path_pwd = realpath(pwd, NULL); + + if (resolved_path_cwd != NULL && + resolved_path_pwd != NULL && + streq(resolved_path_cwd, resolved_path_pwd)) + { + /* + * The resolved path of $PWD matches the resolved path of + * the *actual* working directory - use $PWD. + * + * This makes a difference when $PWD refers to a symlink. + */ + cwd = pwd; + } + + free(resolved_path_cwd); + free(resolved_path_pwd); + } + + shm_set_max_pool_size(conf.tweak.max_shm_pool_size); + shm_set_min_stride_alignment(conf.tweak.min_stride_alignment); + + if ((fdm = fdm_init()) == NULL) + goto out; + + if ((reaper = reaper_init(fdm)) == NULL) + goto out; + + if ((key_binding_manager = key_binding_manager_new()) == NULL) + goto out; + + if ((wayl = wayl_init( + fdm, key_binding_manager, conf.presentation_timings)) == NULL) + { + goto out; + } + + if ((renderer = render_init(fdm, wayl)) == NULL) + goto out; + + if (!as_server && (term = term_init( + &conf, fdm, reaper, wayl, "foot", cwd, token, pty_path, + argc, argv, NULL, + &term_shutdown_cb, &shutdown_ctx)) == NULL) { + goto out; + } + free(_cwd); + _cwd = NULL; + + if (as_server && (server = server_init(&conf, fdm, reaper, wayl)) == NULL) + goto out; + + volatile sig_atomic_t aborted = false; + if (!fdm_signal_add(fdm, SIGINT, &fdm_sigint, (void *)&aborted) || + !fdm_signal_add(fdm, SIGTERM, &fdm_sigint, (void *)&aborted)) + { + goto out; + } + + struct sigusr_context sigusr_context = { + .term = term, + .server = server, + }; + + if (!fdm_signal_add(fdm, SIGUSR1, &fdm_sigusr, &sigusr_context) || + !fdm_signal_add(fdm, SIGUSR2, &fdm_sigusr, &sigusr_context)) + { + goto out; + } + + struct sigaction sig_ign = {.sa_handler = SIG_IGN}; + sigemptyset(&sig_ign.sa_mask); + if (sigaction(SIGHUP, &sig_ign, NULL) < 0 || + sigaction(SIGPIPE, &sig_ign, NULL) < 0) + { + LOG_ERRNO("failed to ignore SIGHUP+SIGPIPE"); + goto out; + } + + if (as_server) + LOG_INFO("running as server; launch terminals by running footclient"); + + if (as_server && pid_file != NULL) { + if (!print_pid(pid_file, &unlink_pid_file)) + goto out; + } + + ret = EXIT_SUCCESS; + while (likely(!aborted && (as_server || tll_length(wayl->terms) > 0))) { + if (unlikely(!fdm_poll(fdm))) { + ret = foot_exit_failure; + break; + } + } + +out: + free(_cwd); + server_destroy(server); + term_destroy(term); + + shm_fini(); + render_destroy(renderer); + wayl_destroy(wayl); + key_binding_manager_destroy(key_binding_manager); + reaper_destroy(reaper); + fdm_signal_del(fdm, SIGUSR1); + fdm_signal_del(fdm, SIGUSR2); + fdm_signal_del(fdm, SIGTERM); + fdm_signal_del(fdm, SIGINT); + fdm_destroy(fdm); + + config_free(&conf); + + if (unlink_pid_file) + unlink(pid_file); + + LOG_INFO("goodbye"); + fcft_fini(); + log_deinit(); + return ret == EXIT_SUCCESS && !as_server ? shutdown_ctx.exit_code : ret; +} + +UNITTEST +{ + char *s = xstrjoin("foo", "bar"); + xassert(streq(s, "foobar")); + free(s); + + s = xstrjoin3("foo", " ", "bar"); + xassert(streq(s, "foo bar")); + free(s); + + s = xstrjoin3("foo", ",", "bar"); + xassert(streq(s, "foo,bar")); + free(s); + + s = xstrjoin3("foo", "bar", "baz"); + xassert(streq(s, "foobarbaz")); + free(s); +} diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..a0e602b --- /dev/null +++ b/meson.build @@ -0,0 +1,454 @@ +project('foot', 'c', + version: '1.26.1', + license: 'MIT', + meson_version: '>=0.59.0', + default_options: [ + 'c_std=c11', + 'warning_level=1', + 'werror=true', + 'b_ndebug=if-release']) + +is_debug_build = get_option('buildtype').startswith('debug') + +cc = meson.get_compiler('c') + +# Newer clang versions warns when using __COUNTER__ without -std=c2y +if cc.has_argument('-Wc2y-extensions') + add_project_arguments('-Wno-c2y-extensions', language: 'c') +endif + +if cc.has_function('memfd_create', + args: ['-D_GNU_SOURCE'], + prefix: '#include ') + add_project_arguments('-DMEMFD_CREATE', language: 'c') +endif + +# Missing on DragonFly, FreeBSD < 14.1 +if cc.has_function('execvpe', + args: ['-D_GNU_SOURCE'], + prefix: '#include ') + add_project_arguments('-DEXECVPE', language: 'c') +endif + +if cc.has_function('sigabbrev_np', + args: ['-D_GNU_SOURCE'], + prefix: '#include ') + add_project_arguments('-DSIGABBREV_NP', language: 'c') +endif + +utmp_backend = get_option('utmp-backend') +if utmp_backend == 'auto' + host_os = host_machine.system() + if host_os == 'linux' + utmp_backend = 'libutempter' + elif host_os == 'freebsd' + utmp_backend = 'ulog' + else + utmp_backend = 'none' + endif +endif + +utmp_default_helper_path = get_option('utmp-default-helper-path') + +if utmp_backend == 'none' + utmp_add = '' + utmp_del = '' + utmp_del_have_argument = false + utmp_default_helper_path = '' +elif utmp_backend == 'libutempter' + utmp_add = 'add' + utmp_del = 'del' + utmp_del_have_argument = false + if utmp_default_helper_path == 'auto' + utmp_default_helper_path = join_paths('/usr', get_option('libdir'), 'utempter', 'utempter') + endif +elif utmp_backend == 'ulog' + utmp_add = 'login' + utmp_del = 'logout' + utmp_del_have_argument = false + if utmp_default_helper_path == 'auto' + utmp_default_helper_path = join_paths('/usr', get_option('libexecdir'), 'ulog-helper') + endif +else + error('invalid utmp backend') +endif + +add_project_arguments( + ['-D_GNU_SOURCE=200809L', + '-DFOOT_DEFAULT_TERM="@0@"'.format(get_option('default-terminfo'))] + + (utmp_backend != 'none' + ? ['-DUTMP_ADD="@0@"'.format(utmp_add), + '-DUTMP_DEL="@0@"'.format(utmp_del), + '-DUTMP_DEFAULT_HELPER_PATH="@0@"'.format(utmp_default_helper_path)] + : []) + + (utmp_del_have_argument + ? ['-DUTMP_DEL_HAVE_ARGUMENT=1'] + : []) + + (is_debug_build + ? ['-D_DEBUG'] + : [cc.get_supported_arguments('-fno-asynchronous-unwind-tables')]) + + (get_option('ime') + ? ['-DFOOT_IME_ENABLED=1'] + : []) + + (get_option('b_pgo') == 'use' + ? ['-DFOOT_PGO_ENABLED=1'] + : []) + + cc.get_supported_arguments( + ['-pedantic', + '-fstrict-aliasing', + '-Wstrict-aliasing']), + language: 'c', +) + +terminfo_install_location = get_option('custom-terminfo-install-location') + +if terminfo_install_location != '' + add_project_arguments( + ['-DFOOT_TERMINFO_PATH="@0@"'.format( + join_paths(get_option('prefix'), terminfo_install_location))], + language: 'c') +else + terminfo_install_location = join_paths(get_option('datadir'), 'terminfo') +endif + +# Compute the relative path used by compiler invocations. +source_root = meson.current_source_dir().split('/') +build_root = meson.global_build_root().split('/') +relative_dir_parts = [] +i = 0 +in_prefix = true +foreach p : build_root + if i >= source_root.length() or not in_prefix or p != source_root[i] + in_prefix = false + relative_dir_parts += '..' + endif + i += 1 +endforeach +i = 0 +in_prefix = true +foreach p : source_root + if i >= build_root.length() or not in_prefix or build_root[i] != p + in_prefix = false + relative_dir_parts += p + endif + i += 1 +endforeach +relative_dir = join_paths(relative_dir_parts) + '/' + +if cc.has_argument('-fmacro-prefix-map=/foo=') + add_project_arguments('-fmacro-prefix-map=@0@='.format(relative_dir), language: 'c') +endif + +math = cc.find_library('m') +threads = [dependency('threads'), cc.find_library('stdthreads', required: false)] +libepoll = dependency('epoll-shim', required: false) +pixman = dependency('pixman-1') +wayland_protocols = dependency('wayland-protocols', version: '>=1.41', + fallback: 'wayland-protocols', + default_options: ['tests=false']) +wayland_client = dependency('wayland-client') +wayland_cursor = dependency('wayland-cursor') +xkb = dependency('xkbcommon', version: '>=1.0.0') +fontconfig = dependency('fontconfig') +utf8proc = dependency('libutf8proc', required: get_option('grapheme-clustering')) + +if utf8proc.found() + add_project_arguments('-DFOOT_GRAPHEME_CLUSTERING=1', language: 'c') +endif + +if pixman.version().version_compare('>=0.46.0') + add_project_arguments('-DHAVE_PIXMAN_RGBA_16', language: 'c') +endif + +tllist = dependency('tllist', version: '>=1.1.0', fallback: 'tllist') +fcft = dependency('fcft', version: ['>=3.3.1', '<4.0.0'], fallback: 'fcft') + +wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir') + +wscanner = dependency('wayland-scanner', native: true) +wscanner_prog = find_program( + wscanner.get_variable('wayland_scanner'), native: true) + +wl_proto_headers = [] +wl_proto_src = [] +wl_proto_xml = [ + wayland_protocols_datadir / 'stable/xdg-shell/xdg-shell.xml', + wayland_protocols_datadir / 'unstable/xdg-decoration/xdg-decoration-unstable-v1.xml', + wayland_protocols_datadir / 'unstable/xdg-output/xdg-output-unstable-v1.xml', + wayland_protocols_datadir / 'unstable/primary-selection/primary-selection-unstable-v1.xml', + wayland_protocols_datadir / 'stable/presentation-time/presentation-time.xml', + wayland_protocols_datadir / 'unstable/text-input/text-input-unstable-v3.xml', + wayland_protocols_datadir / 'staging/xdg-activation/xdg-activation-v1.xml', + wayland_protocols_datadir / 'stable/viewporter/viewporter.xml', + wayland_protocols_datadir / 'staging/fractional-scale/fractional-scale-v1.xml', + wayland_protocols_datadir / 'unstable/tablet/tablet-unstable-v2.xml', # required by cursor-shape-v1 + wayland_protocols_datadir / 'staging/cursor-shape/cursor-shape-v1.xml', + wayland_protocols_datadir / 'staging/single-pixel-buffer/single-pixel-buffer-v1.xml', + wayland_protocols_datadir / 'staging/xdg-toplevel-icon/xdg-toplevel-icon-v1.xml', + wayland_protocols_datadir / 'staging/xdg-system-bell/xdg-system-bell-v1.xml', + wayland_protocols_datadir / 'staging/color-management/color-management-v1.xml', +] + +if (wayland_protocols.version().version_compare('>=1.43')) + wl_proto_xml += [wayland_protocols_datadir / 'staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml'] + add_project_arguments('-DHAVE_XDG_TOPLEVEL_TAG=1', language: 'c') +endif +if (wayland_protocols.version().version_compare('>=1.45')) + wl_proto_xml += [wayland_protocols_datadir / 'staging/ext-background-effect/ext-background-effect-v1.xml'] + add_project_arguments('-DHAVE_EXT_BACKGROUND_EFFECT=1', language: 'c') +endif + +foreach prot : wl_proto_xml + wl_proto_headers += custom_target( + prot.underscorify() + '-client-header', + output: '@BASENAME@.h', + input: prot, + command: [wscanner_prog, 'client-header', '@INPUT@', '@OUTPUT@']) + + wl_proto_src += custom_target( + prot.underscorify() + '-private-code', + output: '@BASENAME@.c', + input: prot, + command: [wscanner_prog, 'private-code', '@INPUT@', '@OUTPUT@']) +endforeach + +env = find_program('env', native: true) +generate_version_sh = files('generate-version.sh') +version = custom_target( + 'generate_version', + build_always_stale: true, + output: 'version.h', + command: [env, 'LC_ALL=C', generate_version_sh, meson.project_version(), '@CURRENT_SOURCE_DIR@', '@OUTPUT@']) + +python = find_program('python3', native: true) +generate_builtin_terminfo_py = files('scripts/generate-builtin-terminfo.py') +foot_terminfo = files('foot.info') +builtin_terminfo = custom_target( + 'generate_builtin_terminfo', + output: 'foot-terminfo.h', + command: [python, generate_builtin_terminfo_py, + '@default_terminfo@', foot_terminfo, 'foot', '@OUTPUT@'] +) + +generate_emoji_variation_sequences = files('scripts/generate-emoji-variation-sequences.py') +emoji_variation_sequences = custom_target( + 'generate_emoji_variation_sequences', + input: 'unicode/emoji-variation-sequences.txt', + output: 'emoji-variation-sequences.h', + command: [python, generate_emoji_variation_sequences, '@INPUT@', '@OUTPUT@'] +) + +generate_srgb_funcs = files('scripts/srgb.py') +srgb_funcs = custom_target( + 'generate_srgb_funcs', + output: ['srgb.c', 'srgb.h'], + command: [python, generate_srgb_funcs, '@OUTPUT0@', '@OUTPUT1@'] +) + +common = static_library( + 'common', + 'log.c', 'log.h', + 'char32.c', 'char32.h', + 'debug.c', 'debug.h', + 'macros.h', + 'xmalloc.c', 'xmalloc.h', + 'xsnprintf.c', 'xsnprintf.h', + dependencies: [utf8proc] +) + +misc = static_library( + 'misc', + 'hsl.c', 'hsl.h', + 'macros.h', + 'misc.c', 'misc.h', + 'uri.c', 'uri.h', + dependencies: [utf8proc], + link_with: [common] +) + +vtlib = static_library( + 'vtlib', + 'base64.c', 'base64.h', + 'composed.c', 'composed.h', + 'cursor-shape.c', 'cursor-shape.h', + 'csi.c', 'csi.h', + 'dcs.c', 'dcs.h', + 'macros.h', + 'osc.c', 'osc.h', + 'sixel.c', 'sixel.h', + 'vt.c', 'vt.h', + builtin_terminfo, srgb_funcs, + wl_proto_src + wl_proto_headers, + version, + dependencies: [libepoll, pixman, fcft, tllist, wayland_client, xkb, utf8proc], + link_with: [common, misc], +) + +pgolib = static_library( + 'pgolib', + 'grid.c', 'grid.h', + 'selection.c', 'selection.h', + 'terminal.c', 'terminal.h', + emoji_variation_sequences, + wl_proto_src + wl_proto_headers, + dependencies: [libepoll, pixman, fcft, tllist, wayland_client, xkb, utf8proc], + link_with: vtlib, +) + +tokenize = static_library( + 'tokenizelib', + 'tokenize.c', + dependencies: [utf8proc], + link_with: [common], +) + +if get_option('b_pgo') == 'generate' + executable( + 'pgo', + 'pgo/pgo.c', + wl_proto_src + wl_proto_headers, + dependencies: [math, threads, libepoll, pixman, wayland_client, xkb, utf8proc, fcft, tllist], + link_with: pgolib, + ) +endif + +executable( + 'foot', + 'async.c', 'async.h', + 'box-drawing.c', 'box-drawing.h', + 'config.c', 'config.h', + 'commands.c', 'commands.h', + 'extract.c', 'extract.h', + 'fdm.c', 'fdm.h', + 'foot-features.c', 'foot-features.h', + 'ime.c', 'ime.h', + 'input.c', 'input.h', + 'key-binding.c', 'key-binding.h', + 'main.c', + 'notify.c', 'notify.h', + 'quirks.c', 'quirks.h', + 'reaper.c', 'reaper.h', + 'render.c', 'render.h', + 'search.c', 'search.h', + 'server.c', 'server.h', 'client-protocol.h', + 'shm.c', 'shm.h', + 'slave.c', 'slave.h', + 'spawn.c', 'spawn.h', + 'tokenize.c', 'tokenize.h', + 'unicode-mode.c', 'unicode-mode.h', + 'url-mode.c', 'url-mode.h', + 'user-notification.c', 'user-notification.h', + 'wayland.c', 'wayland.h', 'shm-formats.h', + 'xkbcommon-vmod.h', + srgb_funcs, wl_proto_src + wl_proto_headers, version, + dependencies: [math, threads, libepoll, pixman, wayland_client, wayland_cursor, xkb, fontconfig, utf8proc, + tllist, fcft], + link_with: pgolib, + install: true) + +executable( + 'footclient', + 'client.c', 'client-protocol.h', + 'foot-features.c', 'foot-features.h', + 'macros.h', + 'util.h', + version, + dependencies: [tllist, utf8proc], + link_with: common, + install: true) + +install_data( + 'foot.desktop', 'foot-server.desktop', 'footclient.desktop', + install_dir: join_paths(get_option('datadir'), 'applications')) + +systemd = dependency('systemd', required: false) +custom_systemd_units_dir = get_option('systemd-units-dir') + +if systemd.found() or custom_systemd_units_dir != '' + configuration = configuration_data() + configuration.set('bindir', join_paths(get_option('prefix'), get_option('bindir'))) + + if (custom_systemd_units_dir == '') + systemd_units_dir = systemd.get_variable('systemduserunitdir') + else + systemd_units_dir = custom_systemd_units_dir + endif + + configure_file( + configuration: configuration, + input: 'foot-server.service.in', + output: '@BASENAME@', + install_dir: systemd_units_dir + ) + + install_data( + 'foot-server.socket', + install_dir: systemd_units_dir) +endif + +scdoc = dependency('scdoc', native: true, required: get_option('docs')) +install_data('foot.ini', install_dir: join_paths(get_option('sysconfdir'), 'xdg', 'foot')) +if scdoc.found() + install_data( + 'LICENSE', 'README.md', 'CHANGELOG.md', + install_dir: join_paths(get_option('datadir'), 'doc', 'foot')) + subdir('doc') +endif + +if get_option('themes') + install_subdir('themes', install_dir: join_paths(get_option('datadir'), 'foot')) +endif + +terminfo_base_name = get_option('terminfo-base-name') +if terminfo_base_name == '' + terminfo_base_name = get_option('default-terminfo') +endif + +tic = find_program('tic', native: true, required: get_option('terminfo')) +if tic.found() + conf_data = configuration_data( + { + 'default_terminfo': terminfo_base_name + } + ) + + preprocessed = configure_file( + input: 'foot.info', + output: 'foot.info.preprocessed', + configuration: conf_data, + ) + custom_target( + 'terminfo', + output: terminfo_base_name[0], + input: preprocessed, + command: [tic, '-x', '-o', '@OUTDIR@', '-e', '@0@,@0@-direct'.format(terminfo_base_name), '@INPUT@'], + install: true, + install_dir: terminfo_install_location + ) +endif + +subdir('completions') +subdir('icons') +subdir('utils') + +if (get_option('tests')) + subdir('tests') +endif + +summary( + { + 'Documentation': scdoc.found(), + 'Themes': get_option('themes'), + 'IME': get_option('ime'), + 'Grapheme clustering': utf8proc.found(), + 'utmp backend': utmp_backend, + 'utmp helper default path': utmp_default_helper_path, + 'Build terminfo': tic.found(), + 'Terminfo base name': terminfo_base_name, + 'Terminfo install location': terminfo_install_location, + 'Default TERM': get_option('default-terminfo'), + 'Set TERMINFO': get_option('custom-terminfo-install-location') != '', + 'Build tests': get_option('tests'), + }, + bool_yn: true +) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..ab7a07b --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,29 @@ +option('docs', type: 'feature', + description: 'Build and install documentation (man pages, example foot.ini, readme, changelog, license etc).') + +option('themes', type: 'boolean', value: true, + description: 'Install themes (predefined color schemes)') + +option('ime', type: 'boolean', value: true, + description: 'IME (Input Method Editor) support') + +option('grapheme-clustering', type: 'feature', + description: 'Enables grapheme clustering using libutf8proc. Requires fcft with harfbuzz support to be useful.') + +option('tests', type: 'boolean', value: true, description: 'Build tests') + +option('terminfo', type: 'feature', value: 'enabled', description: 'Build and install foot\'s terminfo files.') +option('default-terminfo', type: 'string', value: 'foot', + description: 'Default value of the "term" option in foot.ini.') +option('terminfo-base-name', type: 'string', + description: 'Base name of the generated terminfo files. Defaults to the value of the \'default-terminfo\' meson option') +option('custom-terminfo-install-location', type: 'string', value: '', + description: 'Path to foot\'s terminfo, relative to ${prefix}. If set, foot will set $TERMINFO to this value in the client process.') + +option('systemd-units-dir', type: 'string', value: '', + description: 'Where to install the systemd service files (absolute path). Default: ${systemduserunitdir}') + +option('utmp-backend', type: 'combo', value: 'auto', choices: ['none', 'libutempter', 'ulog', 'auto'], + description: 'Which utmp logging backend to use. This affects how (with what arguments) the utmp helper binary (see \'utmp-default-helper-path\')is called. Default: auto (linux=libutempter, freebsd=ulog, others=none)') +option('utmp-default-helper-path', type: 'string', value: 'auto', + description: 'Default path to the utmp helper binary. Default: auto-detect') diff --git a/misc.c b/misc.c new file mode 100644 index 0000000..1369df0 --- /dev/null +++ b/misc.c @@ -0,0 +1,63 @@ +#include "misc.h" +#include "char32.h" +#include + +bool +isword(char32_t wc, bool spaces_only, const char32_t *delimiters) +{ + if (spaces_only) + return isc32graph(wc); + + if (c32chr(delimiters, wc) != NULL) + return false; + + return isc32graph(wc); +} + +void +timespec_add(const struct timespec *a, const struct timespec *b, + struct timespec *res) +{ + const long one_sec_in_ns = 1000000000; + + res->tv_sec = a->tv_sec + b->tv_sec; + res->tv_nsec = a->tv_nsec + b->tv_nsec; + /* tv_nsec may be negative */ + if (res->tv_nsec >= one_sec_in_ns) { + res->tv_sec++; + res->tv_nsec -= one_sec_in_ns; + } +} + +void +timespec_sub(const struct timespec *a, const struct timespec *b, + struct timespec *res) +{ + const long one_sec_in_ns = 1000000000; + + res->tv_sec = a->tv_sec - b->tv_sec; + res->tv_nsec = a->tv_nsec - b->tv_nsec; + /* tv_nsec may be negative */ + if (res->tv_nsec < 0) { + res->tv_sec--; + res->tv_nsec += one_sec_in_ns; + } +} + +bool +is_valid_utf8_and_printable(const char *value) +{ + char32_t *wide = ambstoc32(value); + if (wide == NULL) + return false; + + for (const char32_t *c = wide; *c != U'\0'; c++) { + if (!isc32print(*c)) { + free(wide); + return false; + } + } + + free(wide); + return true; +} diff --git a/misc.h b/misc.h new file mode 100644 index 0000000..6c77c48 --- /dev/null +++ b/misc.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include + +bool isword(char32_t wc, bool spaces_only, const char32_t *delimiters); + +void timespec_add(const struct timespec *a, const struct timespec *b, struct timespec *res); +void timespec_sub(const struct timespec *a, const struct timespec *b, struct timespec *res); + +bool is_valid_utf8_and_printable(const char *value); diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000..345516c --- /dev/null +++ b/notes.txt @@ -0,0 +1,6 @@ +1. uses wrong cursor when switching to another tab, only first tab shows +the correct cursor. +2. if i resize a tab the next tab will be the same +size it was previously until i resize the window, but then i will have +to do that for each consecutive window. if i dont resize the window at +all the tabs look fine. diff --git a/notify.c b/notify.c new file mode 100644 index 0000000..e454b03 --- /dev/null +++ b/notify.c @@ -0,0 +1,765 @@ +#include "notify.h" + +#include +#include +#include +#include + +#include +#include +#include + +#define LOG_MODULE "notify" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "config.h" +#include "spawn.h" +#include "terminal.h" +#include "util.h" +#include "wayland.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +void +notify_free(struct terminal *term, struct notification *notif) +{ + if (notif->pid > 0) + fdm_del(term->fdm, notif->stdout_fd); + + free(notif->id); + free(notif->title); + free(notif->body); + free(notif->category); + free(notif->app_id); + free(notif->icon_cache_id); + free(notif->icon_symbolic_name); + free(notif->icon_data); + free(notif->sound_name); + free(notif->xdg_token); + free(notif->stdout_data); + + tll_free_and_free(notif->actions, free); + + if (notif->icon_path != NULL) { + unlink(notif->icon_path); + free(notif->icon_path); + + if (notif->icon_fd >= 0) + close(notif->icon_fd); + } + + memset(notif, 0, sizeof(*notif)); +} + +static bool +write_icon_file(const void *data, size_t data_sz, int *fd, char **filename, + char **symbolic_name) +{ + xassert(*filename == NULL); + xassert(*symbolic_name == NULL); + + char name[64] = "/tmp/foot-notification-icon-XXXXXX"; + + *filename = NULL; + *symbolic_name = NULL; + *fd = mkostemp(name, O_CLOEXEC); + + if (*fd < 0) { + LOG_ERRNO("failed to create temporary file for icon cache"); + return false; + } + + if (write(*fd, data, data_sz) != (ssize_t)data_sz) { + LOG_ERRNO("failed to write icon data to temporary file"); + close(*fd); + *fd = -1; + return false; + } + + LOG_DBG("wrote icon data to %s", name); + *filename = xstrdup(name); + *symbolic_name = xstrjoin("file://", *filename); + return true; +} + +static bool +to_integer(const char *line, size_t len, uint32_t *res) +{ + bool is_id = true; + uint32_t maybe_id = 0; + + for (size_t i = 0; i < len; i++) { + char digit = line[i]; + if (digit < '0' || digit > '9') { + is_id = false; + break; + } + + maybe_id *= 10; + maybe_id += digit - '0'; + } + + *res = maybe_id; + return is_id; +} + +static void +consume_stdout(struct notification *notif, bool eof) +{ + char *data = notif->stdout_data; + const char *line = data; + size_t left = notif->stdout_sz; + + /* Process stdout, line-by-line */ + while (left > 0) { + line = data; + size_t len = left; + char *eol = (char *)memchr(line, '\n', left); + + if (eol != NULL) { + *eol = '\0'; + len = strlen(line); + data = eol + 1; + } else if (!eof) + break; + + uint32_t maybe_id = 0; + uint32_t maybe_button_nr = 0; + + /* Check for daemon assigned ID, either '123', or 'id=123' */ + if ((notif->external_id == 0 && to_integer(line, len, &maybe_id)) || + (len > 3 && memcmp(line, "id=", 3) == 0 && + to_integer(&line[3], len - 3, &maybe_id))) + { + notif->external_id = maybe_id; + LOG_DBG("external ID: %u", notif->external_id); + } + + /* Check for triggered action, either 'default' or 'action=default' */ + else if ((len == 7 && memcmp(line, "default", 7) == 0) || + (len == 7 + 7 && memcmp(line, "action=default", 7 + 7) == 0)) + { + notif->activated = true; + LOG_DBG("notification's default action was triggered"); + } + + else if (len > 7 && memcmp(line, "action=", 7) == 0) { + notif->activated = true; + + if (to_integer(&line[7], len - 7, &maybe_button_nr)) { + notif->activated_button = maybe_button_nr; + LOG_DBG("custom action %u triggered", notif->activated_button); + } else { + LOG_DBG("unrecognized action triggered: %.*s", + (int)(len - 7), &line[7]); + } + } + + else if (notif->external_id > 0 && + to_integer(line, len, &maybe_button_nr) && + maybe_button_nr > 0 && + maybe_button_nr <= notif->button_count) + { + /* Single integer, appearing *after* the ID, and is within + the custom button/action range */ + notif->activated = true; + notif->activated_button = maybe_button_nr; + LOG_DBG("custom action %u triggered", notif->activated_button); + } + + /* Check for XDG activation token, 'xdgtoken=xyz' */ + else if (len > 9 && memcmp(line, "xdgtoken=", 9) == 0) { + notif->xdg_token = xstrndup(&line[9], len - 9); + LOG_DBG("XDG token: \"%s\"", notif->xdg_token); + } + + left -= len + (eol != NULL ? 1 : 0); + } + + if (left > 0) + memmove(notif->stdout_data, data, left); + + notif->stdout_sz = left; +} + +static bool +fdm_notify_stdout(struct fdm *fdm, int fd, int events, void *data) +{ + const struct terminal *term = data; + struct notification *notif = NULL; + + /* Find notification */ + tll_foreach(term->active_notifications, it) { + if (it->item.stdout_fd == fd) { + notif = &it->item; + break; + } + } + + if (events & EPOLLIN) { + char buf[512]; + ssize_t count = read(fd, buf, sizeof(buf) - 1); + + if (count < 0) { + if (errno == EINTR) + return true; + + LOG_ERRNO("failed to read notification activation token"); + return false; + } + + if (count > 0 && notif != NULL) { + if (notif->stdout_data == NULL) { + xassert(notif->stdout_sz == 0); + notif->stdout_data = xmemdup(buf, count); + } else { + notif->stdout_data = xrealloc(notif->stdout_data, notif->stdout_sz + count); + memcpy(¬if->stdout_data[notif->stdout_sz], buf, count); + } + + notif->stdout_sz += count; + consume_stdout(notif, false); + } + } + + if (events & EPOLLHUP) { + fdm_del(fdm, fd); + if (notif != NULL) { + notif->stdout_fd = -1; + consume_stdout(notif, true); + } + } + + return true; +} + +static void +notif_done(struct reaper *reaper, pid_t pid, int status, void *data) +{ + struct terminal *term = data; + + tll_foreach(term->active_notifications, it) { + struct notification *notif = &it->item; + if (notif->pid != pid) + continue; + + LOG_DBG("notification %s closed", + notif->id != NULL ? notif->id : ""); + + if (notif->activated && notif->focus) { + LOG_DBG("focus window on notification activation: \"%s\"", + notif->xdg_token); + + if (notif->xdg_token == NULL) + LOG_WARN("cannot focus window: no activation token available"); + else + wayl_activate(term->wl, term->window, notif->xdg_token); + } + + if (notif->activated && notif->report_activated) { + LOG_DBG("sending notification activation event to client"); + + const char *id = notif->id != NULL ? notif->id : "0"; + + char button_nr[16] = {0}; + if (notif->activated_button > 0) { + xsnprintf( + button_nr, sizeof(button_nr), "%u", notif->activated_button); + } + + char reply[7 + strlen(id) + 1 + strlen(button_nr) + 2 + 1]; + size_t n = xsnprintf( + reply, sizeof(reply), "\033]99;i=%s;%s\033\\", id, button_nr); + term_to_slave(term, reply, n); + } + + if (notif->report_closed) { + LOG_DBG("sending notification close event to client"); + + const char *id = notif->id != NULL ? notif->id : "0"; + char reply[7 + strlen(id) + 1 + 7 + 1 + 2 + 1]; + size_t n = xsnprintf( + reply, sizeof(reply), "\033]99;i=%s:p=close;\033\\", id); + term_to_slave(term, reply, n); + } + + notify_free(term, notif); + tll_remove(term->active_notifications, it); + return; + } +} + +static bool +expand_action_to_argv(struct terminal *term, const char *name, const char *label, + size_t *argc, char ***argv) +{ + char **expanded = NULL; + size_t count = 0; + + if (!spawn_expand_template( + &term->conf->desktop_notifications.command_action_arg, 2, + (const char *[]){"action-name", "action-label"}, + (const char *[]){name, label}, + &count, &expanded)) + { + return false; + } + + /* Append to the "global" actions argv */ + *argv = xrealloc(*argv, (*argc + count) * sizeof((*argv)[0])); + memcpy(&(*argv)[*argc], expanded, count * sizeof(expanded[0])); + *argc += count; + + free(expanded); + return true; +} + +bool +notify_notify(struct terminal *term, struct notification *notif) +{ + xassert(notif->xdg_token == NULL); + xassert(notif->external_id == 0); + xassert(notif->pid == 0); + xassert(notif->stdout_fd <= 0); + xassert(notif->stdout_data == NULL); + xassert(notif->icon_path == NULL); + xassert(notif->icon_fd <= 0); + + notif->pid = -1; + notif->stdout_fd = -1; + notif->icon_fd = -1; + + if (term->conf->desktop_notifications.command.argv.args == NULL) + return false; + + if ((term->conf->desktop_notifications.inhibit_when_focused || + notif->when != NOTIFY_ALWAYS) + && term->kbd_focus) + { + /* No notifications while we're focused */ + return false; + } + + const char *app_id = notif->app_id != NULL + ? notif->app_id + : term->app_id != NULL + ? term->app_id + : term->conf->app_id; + const char *title = notif->title != NULL ? notif->title : notif->body; + const char *body = notif->title != NULL && notif->body != NULL ? notif->body : ""; + + /* Icon: symbolic name if present, otherwise a filename */ + const char *icon_name_or_path = ""; + + if (notif->icon_cache_id != NULL) { + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + const struct notification_icon *icon = &term->notification_icons[i]; + + if (icon->id != NULL && streq(icon->id, notif->icon_cache_id)) { + /* For now, we set the symbolic name to 'file:///path' + * when using a file based icon. */ + xassert(icon->symbolic_name != NULL); + icon_name_or_path = icon->symbolic_name; + + LOG_DBG("using icon from cache (cache ID: %s): %s", + icon->id, icon_name_or_path); + break; + } + } + } else if (notif->icon_symbolic_name != NULL) { + icon_name_or_path = notif->icon_symbolic_name; + LOG_DBG("using symbolic icon from notification: %s", icon_name_or_path); + } else if (notif->icon_data_sz > 0) { + xassert(notif->icon_data != NULL); + + if (write_icon_file( + notif->icon_data, notif->icon_data_sz, + ¬if->icon_fd, + ¬if->icon_path, + ¬if->icon_symbolic_name)) + icon_name_or_path = notif->icon_symbolic_name; + + LOG_DBG("using icon data from notification: %s", icon_name_or_path); + } + + bool track_notification = notif->focus || + notif->report_activated || + notif->may_be_programatically_closed; + + uint32_t replaces_id = 0; + if (notif->id != NULL) { + tll_foreach(term->active_notifications, it) { + struct notification *existing = &it->item; + + if (existing->id == NULL) + continue; + + /* + * When replacing/updating a notification, we may have + * *multiple* notification helpers running for the "same" + * notification. Make sure only the *last* notification's + * report closed/activated are honored, to avoid sending + * multiple reports. + * + * This also means we cannot 'break' out of the loop - we + * must check *all* notifications. + */ + if (existing->external_id != 0 && streq(existing->id, notif->id)) { + replaces_id = existing->external_id; + existing->report_activated = false; + existing->report_closed = false; + } + } + } + + char replaces_id_str[16]; + xsnprintf(replaces_id_str, sizeof(replaces_id_str), "%u", replaces_id); + + const char *urgency_str = + notif->urgency == NOTIFY_URGENCY_LOW + ? "low" + : notif->urgency == NOTIFY_URGENCY_NORMAL + ? "normal" : "critical"; + + LOG_DBG("notify: title=\"%s\", body=\"%s\", app-id=\"%s\", category=\"%s\", " + "urgency=\"%s\", icon=\"%s\", expires=%d, replaces=%u, muted=%s, " + "sound-name=%s (tracking: %s)", + title, body, app_id, notif->category, urgency_str, icon_name_or_path, + notif->expire_time, replaces_id, + notif->muted ? "yes" : "no", notif->sound_name, + track_notification ? "yes" : "no"); + + xassert(title != NULL); + if (title == NULL) + return false; + + char **argv = NULL; + size_t argc = 0; + char **action_argv = NULL; + size_t action_argc = 0; + + char expire_time[16]; + xsnprintf(expire_time, sizeof(expire_time), "%d", notif->expire_time); + + if (term->conf->desktop_notifications.command_action_arg.argv.args) { + if (!expand_action_to_argv( + term, "default", "Activate", &action_argc, &action_argv)) + { + return false; + } + + size_t action_idx = 1; + tll_foreach(notif->actions, it) { + + /* Custom actions use a numerical name, starting at 1 */ + char name[16]; + xsnprintf(name, sizeof(name), "%zu", action_idx++); + + if (!expand_action_to_argv( + term, name, it->item, &action_argc, &action_argv)) + { + for (size_t i = 0; i < action_argc; i++) + free(action_argv[i]); + free(action_argv); + return false; + } + } + } + + if (!spawn_expand_template( + &term->conf->desktop_notifications.command, 12, + (const char *[]){ + "app-id", "window-title", "icon", "title", "body", "category", + "urgency", "muted", "sound-name", "expire-time", "replace-id", + "action-argument"}, + (const char *[]){ + app_id, term->window_title, icon_name_or_path, title, + body != NULL ? body : "", + notif->category != NULL ? notif->category : "", urgency_str, + notif->muted ? "true" : "false", + notif->sound_name != NULL ? notif->sound_name : "", + expire_time, replaces_id_str, + + /* Custom expansion below, since we need to expand to multiple arguments */ + "${action-argument}"}, + &argc, &argv)) + { + return false; + } + + /* Post-process the expanded argv, and patch in all the --action + arguments we expanded earlier */ + for (size_t i = 0; i < argc; i++) { + if (!streq(argv[i], "${action-argument}")) + continue; + + if (action_argc == 0) { + free(argv[i]); + + /* Remove ${command-argument}, but include terminating NULL */ + memmove(&argv[i], &argv[i + 1], (argc - i) * sizeof(argv[0])); + argc--; + break; + } + + /* Remove the "${action-argument}" entry, add all actions argument + from earlier, but include terminating NULL */ + argv = xrealloc(argv, (argc + action_argc) * sizeof(argv[0])); + + /* Move remaining arguments to after the action arguments */ + memmove(&argv[i + action_argc], + &argv[i + 1], + (argc - i) * sizeof(argv[0])); /* Include terminating NULL */ + + free(argv[i]); /* Free xstrdup("${action-argument}"); */ + + /* Insert the action arguments */ + for (size_t j = 0; j < action_argc; j++) { + argv[i + j] = action_argv[j]; + action_argv[j] = NULL; + } + + argc += action_argc; + argc--; /* The ${action-argument} option has been removed */ + break; + } + + LOG_DBG("notify command:"); + for (size_t i = 0; i < argc; i++) + LOG_DBG(" argv[%zu] = \"%s\"", i, argv[i]); + xassert(argv[argc] == NULL); + + int stdout_fds[2] = {-1, -1}; + if (track_notification) { + if (pipe2(stdout_fds, O_CLOEXEC | O_NONBLOCK) < 0) { + LOG_WARN("failed to create stdout pipe"); + track_notification = false; + /* Non-fatal */ + } else { + tll_push_back(term->active_notifications, *notif); + + /* We've taken over ownership of all data; clear, so that + notify_free() doesn't double free */ + notif->id = NULL; + notif->title = NULL; + notif->body = NULL; + notif->category = NULL; + notif->app_id = NULL; + notif->icon_cache_id = NULL; + notif->icon_symbolic_name = NULL; + notif->icon_data = NULL; + notif->icon_data_sz = 0; + notif->icon_path = NULL; + notif->sound_name = NULL; + notif->icon_fd = -1; + notif->stdout_fd = -1; + struct notification *new_notif = &tll_back(term->active_notifications); + + /* We don't need these anymore. They'll be free:d by the caller */ + new_notif->button_count = tll_length(notif->actions); + memset(&new_notif->actions, 0, sizeof(new_notif->actions)); + notif = new_notif; + } + } + + if (stdout_fds[0] >= 0) { + fdm_add(term->fdm, stdout_fds[0], EPOLLIN, + &fdm_notify_stdout, (void *)term); + } + + /* Redirect stdin to /dev/null, but ignore failure to open */ + int devnull = open("/dev/null", O_RDONLY); + pid_t pid = spawn( + term->reaper, NULL, argv, devnull, stdout_fds[1], -1, + track_notification ? ¬if_done : NULL, (void *)term, NULL); + + if (stdout_fds[1] >= 0) { + /* Close write-end of stdout pipe */ + close(stdout_fds[1]); + } + + if (pid < 0 && stdout_fds[0] >= 0) { + /* Remove FDM callback if we failed to spawn */ + fdm_del(term->fdm, stdout_fds[0]); + } + + if (devnull >= 0) + close(devnull); + + for (size_t i = 0; i < argc; i++) + free(argv[i]); + free(argv); + for (size_t i = 0; i < action_argc; i++) + free(action_argv[i]); + free(action_argv); + + notif->pid = pid; + notif->stdout_fd = stdout_fds[0]; + return true; +} + +void +notify_close(struct terminal *term, const char *id) +{ + xassert(id != NULL); + LOG_DBG("close notification %s", id); + + tll_foreach(term->active_notifications, it) { + const struct notification *notif = &it->item; + if (notif->id == NULL || !streq(notif->id, id)) + continue; + + if (term->conf->desktop_notifications.close.argv.args == NULL) { + LOG_DBG( + "trying to close notification \"%s\" by sending SIGINT to %u", + id, notif->pid); + + if (notif->pid == 0) { + LOG_WARN( + "cannot close notification \"%s\": no helper process running", + id); + } else { + /* Best-effort... */ + kill(notif->pid, SIGINT); + } + } else { + LOG_DBG( + "trying to close notification \"%s\" " + "by running user defined command", id); + + if (notif->external_id == 0) { + LOG_WARN("cannot close notification \"%s\": " + "no daemon assigned notification ID available", id); + return; + } + + char **argv = NULL; + size_t argc = 0; + + char external_id[16]; + xsnprintf(external_id, sizeof(external_id), "%u", notif->external_id); + + if (!spawn_expand_template( + &term->conf->desktop_notifications.close, 1, + (const char *[]){"id"}, + (const char *[]){external_id}, + &argc, &argv)) + { + return; + } + + int devnull = open("/dev/null", O_RDONLY); + spawn( + term->reaper, NULL, argv, devnull, -1, -1, + NULL, (void *)term, NULL); + + if (devnull >= 0) + close(devnull); + + for (size_t i = 0; i < argc; i++) + free(argv[i]); + free(argv); + } + + return; + } + + LOG_WARN("cannot close notification \"%s\": no such notification", id); +} + +static void +add_icon(struct notification_icon *icon, const char *id, const char *symbolic_name, + const uint8_t *data, size_t data_sz) +{ + icon->id = xstrdup(id); + icon->symbolic_name = symbolic_name != NULL ? xstrdup(symbolic_name) : NULL; + icon->tmp_file_name = NULL; + icon->tmp_file_fd = -1; + + /* + * Dump in-line data to a temporary file. This allows us to pass + * the filename as a parameter to notification helpers + * (i.e. notify-send -i ). + * + * Optimization: since we always prefer (i.e. use) the symbolic + * name if present, there's no need to create a file on disk if we + * have a symbolic name. + */ + if (symbolic_name == NULL && data_sz > 0) { + write_icon_file( + data, data_sz, + &icon->tmp_file_fd, + &icon->tmp_file_name, + &icon->symbolic_name); + } + + LOG_DBG("added icon to cache: ID=%s: sym=%s, file=%s", + icon->id, icon->symbolic_name, icon->tmp_file_name); +} + +void +notify_icon_add(struct terminal *term, const char *id, + const char *symbolic_name, const uint8_t *data, size_t data_sz) +{ +#if defined(_DEBUG) + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + struct notification_icon *icon = &term->notification_icons[i]; + if (icon->id != NULL && streq(icon->id, id)) { + BUG("notification icon cache already contains \"%s\"", id); + } + } +#endif + + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + struct notification_icon *icon = &term->notification_icons[i]; + if (icon->id == NULL) { + add_icon(icon, id, symbolic_name, data, data_sz); + return; + } + } + + /* Cache full - throw out first entry, add new entry last */ + notify_icon_free(&term->notification_icons[0]); + memmove(&term->notification_icons[0], + &term->notification_icons[1], + ((ALEN(term->notification_icons) - 1) * + sizeof(term->notification_icons[0]))); + + add_icon( + &term->notification_icons[ALEN(term->notification_icons) - 1], + id, symbolic_name, data, data_sz); +} + +void +notify_icon_del(struct terminal *term, const char *id) +{ + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + struct notification_icon *icon = &term->notification_icons[i]; + + if (icon->id == NULL || !streq(icon->id, id)) + continue; + + LOG_DBG("expelled %s from the notification icon cache", icon->id); + notify_icon_free(icon); + return; + } +} + +void +notify_icon_free(struct notification_icon *icon) +{ + if (icon->tmp_file_name != NULL) { + unlink(icon->tmp_file_name); + if (icon->tmp_file_fd >= 0) + close(icon->tmp_file_fd); + } + + free(icon->id); + free(icon->symbolic_name); + free(icon->tmp_file_name); + + icon->id = NULL; + icon->symbolic_name = NULL; + icon->tmp_file_name = NULL; + icon->tmp_file_fd = -1; +} diff --git a/notify.h b/notify.h new file mode 100644 index 0000000..89b5123 --- /dev/null +++ b/notify.h @@ -0,0 +1,95 @@ +#pragma once +#include +#include +#include + +#include + +struct terminal; + +enum notify_when { + /* First, so that it can be left out of initializer and still be + the default */ + NOTIFY_ALWAYS, + + NOTIFY_UNFOCUSED, + NOTIFY_INVISIBLE +}; + +enum notify_urgency { + /* First, so that it can be left out of initializer and still be + the default */ + NOTIFY_URGENCY_NORMAL, + + NOTIFY_URGENCY_LOW, + NOTIFY_URGENCY_CRITICAL, +}; + +struct notification { + /* + * Set by caller of notify_notify() + */ + char *id; /* Internal notification ID */ + + char *app_id; /* Custom app-id, overrides the terminal's app-id if set */ + char *title; /* Required */ + char *body; + char *category; + + enum notify_when when; + enum notify_urgency urgency; + int32_t expire_time; + + tll(char *) actions; + + char *icon_cache_id; + char *icon_symbolic_name; + uint8_t *icon_data; + size_t icon_data_sz; + + bool focus; /* Focus the foot window when notification is activated */ + bool may_be_programatically_closed; /* OSC-99: notification may be programmatically closed by the client */ + bool report_activated; /* OSC-99: report notification activation to client */ + bool report_closed; /* OSC-99: report notification closed to client */ + + bool muted; /* Explicitly mute the notification */ + char *sound_name; /* Should be set to NULL if muted == true */ + + /* + * Used internally by notify + */ + + uint32_t external_id; /* Daemon assigned notification ID */ + bool activated; /* User 'activated' the notification */ + uint32_t button_count; /* Number of buttons (custom actions) in notification */ + uint32_t activated_button; /* User activated one of the custom actions */ + char *xdg_token; /* XDG activation token, from daemon */ + + pid_t pid; /* Notifier command PID */ + int stdout_fd; /* Notifier command's stdout */ + + char *stdout_data; /* Data we've reado from command's stdout */ + size_t stdout_sz; + + /* Used when notification provides raw icon data, and it's + bypassing the icon cache */ + char *icon_path; + int icon_fd; +}; + +struct notification_icon { + char *id; + char *symbolic_name; + char *tmp_file_name; + int tmp_file_fd; +}; + +bool notify_notify(struct terminal *term, struct notification *notif); +void notify_close(struct terminal *term, const char *id); +void notify_free(struct terminal *term, struct notification *notif); + +void notify_icon_add(struct terminal *term, const char *id, + const char *symbolic_name, const uint8_t *data, + size_t data_sz); +void notify_icon_del(struct terminal *term, const char *id); +void notify_icon_free(struct notification_icon *icon); diff --git a/osc.c b/osc.c new file mode 100644 index 0000000..90704b7 --- /dev/null +++ b/osc.c @@ -0,0 +1,1735 @@ +#include "osc.h" + +#include +#include +#include +#include + +#include + +#define LOG_MODULE "osc" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "base64.h" +#include "config.h" +#include "macros.h" +#include "notify.h" +#include "selection.h" +#include "render.h" +#include "terminal.h" +#include "uri.h" +#include "util.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +#define UNHANDLED() LOG_DBG("unhandled: OSC: %.*s", (int)term->vt.osc.idx, term->vt.osc.data) + +static void +osc_to_clipboard(struct terminal *term, const char *target, + const char *base64_data) +{ + bool to_clipboard = false; + bool to_primary = false; + + if (target[0] == '\0') + to_clipboard = true; + + for (const char *t = target; *t != '\0'; t++) { + switch (*t) { + case 'c': + to_clipboard = true; + break; + + case 's': + case 'p': + to_primary = true; + break; + + default: + LOG_WARN("unimplemented: clipboard target '%c'", *t); + break; + } + } + + /* Find a seat in which the terminal has focus */ + struct seat *seat = NULL; + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) { + seat = &it->item; + break; + } + } + + if (seat == NULL) { + LOG_WARN("OSC52: client tried to write to clipboard data while window was unfocused"); + return; + } + + const bool copy_allowed = term->conf->security.osc52 == OSC52_ENABLED + || term->conf->security.osc52 == OSC52_COPY_ENABLED; + + if (!copy_allowed) { + LOG_DBG("ignoring copy request: disabled in configuration"); + return; + } + + char *decoded = base64_decode(base64_data, NULL); + if (decoded == NULL || decoded[0] == '\0') { + if (decoded == NULL) { + if (errno == EINVAL) + LOG_WARN("OSC: invalid clipboard data: %s", base64_data); + else + LOG_ERRNO("base64_decode() failed"); + } + + if (to_clipboard) + selection_clipboard_unset(seat); + if (to_primary) + selection_primary_unset(seat); + free(decoded); + return; + } + + LOG_DBG("decoded: %s", decoded); + + if (to_clipboard) { + char *copy = xstrdup(decoded); + if (!text_to_clipboard(seat, term, copy, seat->kbd.serial)) + free(copy); + } + + if (to_primary) { + char *copy = xstrdup(decoded); + if (!text_to_primary(seat, term, copy, seat->kbd.serial)) + free(copy); + } + + free(decoded); +} + +struct clip_context { + struct seat *seat; + struct terminal *term; + uint8_t buf[3]; + int idx; +}; + +static void +from_clipboard_cb(char *text, size_t size, void *user) +{ + struct clip_context *ctx = user; + struct terminal *term = ctx->term; + + xassert(ctx->idx >= 0 && ctx->idx <= 2); + + const char *t = text; + size_t left = size; + + if (ctx->idx > 0) { + for (size_t i = ctx->idx; i < 3 && left > 0; i++, t++, left--) + ctx->buf[ctx->idx++] = *t; + + xassert(ctx->idx <= 3); + if (ctx->idx == 3) { + char *chunk = base64_encode(ctx->buf, 3); + xassert(chunk != NULL); + xassert(strlen(chunk) == 4); + + term_paste_data_to_slave(term, chunk, 4); + free(chunk); + + ctx->idx = 0; + } + } + + if (left == 0) + return; + + xassert(ctx->idx == 0); + + int remaining = left % 3; + for (int i = remaining; i > 0; i--) + ctx->buf[ctx->idx++] = text[size - i]; + xassert(ctx->idx == remaining); + + char *chunk = base64_encode((const uint8_t *)t, left / 3 * 3); + xassert(chunk != NULL); + xassert(strlen(chunk) % 4 == 0); + term_paste_data_to_slave(term, chunk, strlen(chunk)); + free(chunk); +} + +static void +from_clipboard_done(void *user) +{ + struct clip_context *ctx = user; + struct terminal *term = ctx->term; + + if (ctx->idx > 0) { + char res[4]; + base64_encode_final(ctx->buf, ctx->idx, res); + term_paste_data_to_slave(term, res, 4); + } + + if (term->vt.osc.bel) + term_paste_data_to_slave(term, "\a", 1); + else + term_paste_data_to_slave(term, "\033\\", 2); + + term->is_sending_paste_data = false; + + /* Make sure we send any queued up non-paste data */ + if (tll_length(term->ptmx_buffers) > 0) + fdm_event_add(term->fdm, term->ptmx, EPOLLOUT); + + free(ctx); +} + +static void +osc_from_clipboard(struct terminal *term, const char *source) +{ + /* Find a seat in which the terminal has focus */ + struct seat *seat = NULL; + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) { + seat = &it->item; + break; + } + } + + if (seat == NULL) { + LOG_WARN("OSC52: client tried to read clipboard data while window was unfocused"); + return; + } + + const bool paste_allowed = term->conf->security.osc52 == OSC52_ENABLED + || term->conf->security.osc52 == OSC52_PASTE_ENABLED; + if (!paste_allowed) { + LOG_DBG("ignoring paste request: disabled in configuration"); + return; + } + + /* Use clipboard if no source has been specified */ + char src = source[0] == '\0' ? 'c' : 0; + bool from_clipboard = src == 'c'; + bool from_primary = false; + + for (const char *s = source; + *s != '\0' && !from_clipboard && !from_primary; + s++) + { + if (*s == 'c' || *s == 'p' || *s == 's') { + src = *s; + + switch (src) { + case 'c': + from_clipboard = selection_clipboard_has_data(seat); + break; + + case 's': + case 'p': + from_primary = selection_primary_has_data(seat); + break; + } + } else + LOG_WARN("unimplemented: clipboard source '%c'", *s); + } + + if (!from_clipboard && !from_primary) + return; + + if (term->is_sending_paste_data) { + /* FIXME: we should wait for the paste to end, then continue + with the OSC-52 reply */ + term_to_slave(term, "\033]52;", 5); + term_to_slave(term, &src, 1); + term_to_slave(term, ";", 1); + if (term->vt.osc.bel) + term_to_slave(term, "\a", 1); + else + term_to_slave(term, "\033\\", 2); + return; + } + + term->is_sending_paste_data = true; + + term_paste_data_to_slave(term, "\033]52;", 5); + term_paste_data_to_slave(term, &src, 1); + term_paste_data_to_slave(term, ";", 1); + + struct clip_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct clip_context) {.seat = seat, .term = term}; + + if (from_clipboard) { + text_from_clipboard( + seat, term, true, &from_clipboard_cb, &from_clipboard_done, ctx); + } + + if (from_primary) { + text_from_primary( + seat, term, true, &from_clipboard_cb, &from_clipboard_done, ctx); + } +} + +static void +osc_selection(struct terminal *term, char *string) +{ + char *p = string; + bool clipboard_done = false; + + /* The first parameter is a string of clipbard sources/targets */ + while (*p != '\0' && !clipboard_done) { + switch (*p) { + case ';': + clipboard_done = true; + *p = '\0'; + break; + } + + p++; + } + + LOG_DBG("clipboard: target = %s data = %s", string, p); + + if (p[0] == '?' && p[1] == '\0') + osc_from_clipboard(term, string); + else + osc_to_clipboard(term, string, p); +} + +static void +osc_flash(struct terminal *term) +{ + /* Our own private - flash */ + term_flash(term, 50); +} + +static bool +parse_legacy_color(const char *string, uint32_t *color, bool *_have_alpha, + uint16_t *_alpha) +{ + bool have_alpha = false; + uint16_t alpha = 0xffff; + + if (string[0] == '[') { + /* e.g. \E]11;[50]#00ff00 */ + const char *start = &string[1]; + + errno = 0; + char *end; + unsigned long percent = strtoul(start, &end, 10); + + if (errno != 0 || *end != ']') + return false; + + have_alpha = true; + alpha = (0xffff * min(percent, 100) + 50) / 100; + + string = end + 1; + } + + if (string[0] != '#') + return false; + + string++; + const size_t len = strlen(string); + + if (len % 3 != 0) + return false; + + const int digits = len / 3; + + int rgb[3]; + for (size_t i = 0; i < 3; i++) { + rgb[i] = 0; + for (size_t j = 0; j < digits; j++) { + size_t idx = i * digits + j; + char c = string[idx]; + rgb[i] <<= 4; + + if (!isxdigit(c)) + rgb[i] |= 0; + else + rgb[i] |= c >= '0' && c <= '9' ? c - '0' : + c >= 'a' && c <= 'f' ? c - 'a' + 10 : c - 'A' + 10; + } + + /* Values with less than 16 bits represent the *most + * significant bits*. I.e. the values are *not* scaled */ + rgb[i] <<= 16 - (4 * digits); + } + + /* Re-scale to 8-bit */ + uint8_t r = 256 * (rgb[0] / 65536.); + uint8_t g = 256 * (rgb[1] / 65536.); + uint8_t b = 256 * (rgb[2] / 65536.); + + LOG_DBG("legacy: %02x%02x%02x (alpha=%04x)", r, g, b, + have_alpha ? alpha : 0xffff); + + *color = r << 16 | g << 8 | b; + + if (_have_alpha != NULL) + *_have_alpha = have_alpha; + if (_alpha != NULL) + *_alpha = alpha; + return true; +} + +static bool +parse_rgb(const char *string, uint32_t *color, bool *_have_alpha, + uint16_t *_alpha) +{ + size_t len = strlen(string); + bool have_alpha = len >= 4 && strncmp(string, "rgba", 4) == 0; + + /* Verify we have the minimum required length (for "") */ + if (have_alpha) { + if (len < STRLEN("rgba:x/x/x/x")) + return false; + } else { + if (len < STRLEN("rgb:x/x/x")) + return false; + } + + /* Verify prefix is "rgb:" or "rgba:" */ + if (have_alpha) { + if (strncmp(string, "rgba:", 5) != 0) + return false; + string += 5; + len -= 5; + } else { + if (strncmp(string, "rgb:", 4) != 0) + return false; + string += 4; + len -= 4; + } + + int rgb[4]; + int digits[4]; + + for (size_t i = 0; i < (have_alpha ? 4 : 3); i++) { + for (rgb[i] = 0, digits[i] = 0; + len > 0 && *string != '/'; + len--, string++, digits[i]++) + { + char c = *string; + rgb[i] <<= 4; + + if (!isxdigit(c)) + rgb[i] |= 0; + else + rgb[i] |= c >= '0' && c <= '9' ? c - '0' : + c >= 'a' && c <= 'f' ? c - 'a' + 10 : c - 'A' + 10; + } + + if (i >= (have_alpha ? 3 : 2)) + break; + + if (len == 0 || *string != '/') + return false; + string++; len--; + } + + /* Re-scale to 8-bit */ + uint8_t r = 256 * (rgb[0] / (double)(1 << (4 * digits[0]))); + uint8_t g = 256 * (rgb[1] / (double)(1 << (4 * digits[1]))); + uint8_t b = 256 * (rgb[2] / (double)(1 << (4 * digits[2]))); + + uint16_t alpha = 0xffff; + if (have_alpha) + alpha = 65536 * (rgb[3] / (double)(1 << (4 * digits[3]))); + + if (have_alpha) + LOG_DBG("rgba: %02x%02x%02x (alpha=%04x)", r, g, b, alpha); + else + LOG_DBG("rgb: %02x%02x%02x", r, g, b); + + if (_have_alpha != NULL) + *_have_alpha = have_alpha; + if (_alpha != NULL) + *_alpha = alpha; + + *color = r << 16 | g << 8 | b; + return true; +} + +static void +osc_set_pwd(struct terminal *term, char *string) +{ + LOG_DBG("PWD: URI: %s", string); + + char *scheme, *host, *path; + if (!uri_parse(string, strlen(string), &scheme, NULL, NULL, &host, NULL, &path, NULL, NULL)) { + LOG_ERR("OSC7: invalid URI: %s", string); + return; + } + + if (streq(scheme, "file") && hostname_is_localhost(host)) { + LOG_DBG("OSC7: pwd: %s", path); + free(term->cwd); + term->cwd = path; + render_refresh_tab_bar(term); + } else + free(path); + + free(scheme); + free(host); +} + +static void +osc_uri(struct terminal *term, char *string) +{ + /* + * \E]8;;URI\e\\ + * + * Params are key=value pairs, separated by ':'. + * + * The only defined key (as of 2020-05-31) is 'id', which is used + * to group split-up URIs: + * + * ╔═ file1 ════╗ + * ║ ╔═ file2 ═══╗ + * ║http://exa║Lorem ipsum║ + * ║le.com ║ dolor sit ║ + * ║ ║amet, conse║ + * ╚══════════║ctetur adip║ + * ╚═══════════╝ + * + * This lets a terminal emulator highlight both parts at the same + * time (e.g. when hovering over one of the parts with the mouse). + */ + + char *params = string; + char *params_end = strchr(params, ';'); + if (params_end == NULL) + return; + + *params_end = '\0'; + const char *uri = params_end + 1; + uint64_t id = (uint64_t)rand() << 32 | rand(); + + char *ctx = NULL; + for (const char *key_value = strtok_r(params, ":", &ctx); + key_value != NULL; + key_value = strtok_r(NULL, ":", &ctx)) + { + const char *key = key_value; + char *operator = (char *)strchr(key_value, '='); + + if (operator == NULL) + continue; + *operator = '\0'; + + const char *value = operator + 1; + + if (streq(key, "id")) + id = sdbm_hash(value); + } + + + if (uri[0] == '\0') { + LOG_DBG("OSC-8: close"); + term_osc8_close(term); + } else { + LOG_DBG("OSC-8: URL=%s, id=%" PRIu64, uri, id); + term_osc8_open(term, id, uri); + } +} + +static void +osc_notify(struct terminal *term, char *string) +{ + /* + * The 'notify' perl extension + * (https://pub.phyks.me/scripts/urxvt/notify) is very simple: + * + * #!/usr/bin/perl + * + * sub on_osc_seq_perl { + * my ($term, $osc, $resp) = @_; + * if ($osc =~ /^notify;(\S+);(.*)$/) { + * system("notify-send '$1' '$2'"); + * } + * } + * + * As can be seen, the notification text is not encoded in any + * way. The regex does a greedy match of the ';' separator. Thus, + * any extra ';' will end up being part of the title. There's no + * way to have a ';' in the message body. + * + * I've changed that behavior slightly in; we split the title from + * body on the *first* ';', allowing us to have semicolons in the + * message body, but *not* in the title. + */ + char *ctx = NULL; + const char *title = strtok_r(string, ";", &ctx); + const char *msg = strtok_r(NULL, "\x00", &ctx); + + if (title == NULL) + return; + + if (mbsntoc32(NULL, title, strlen(title), 0) == (size_t)-1) { + LOG_WARN("%s: notification title is not valid UTF-8, ignoring", title); + return; + } + + if (msg != NULL && mbsntoc32(NULL, msg, strlen(msg), 0) == (size_t)-1) { + LOG_WARN("%s: notification message is not valid UTF-8, ignoring", msg); + return; + } + + char *msgdup = NULL; + if (msg != NULL) + msgdup = xstrdup(msg); + + notify_notify(term, &(struct notification){ + .title = xstrdup(title), + .body = msgdup, + .expire_time = -1, + .focus = true, + }); +} + +IGNORE_WARNING("-Wpedantic") +static bool +verify_kitty_id_is_valid(const char *id) +{ + const size_t len = strlen(id); + + for (size_t i = 0; i < len; i++) { + switch (id[i]) { + case 'a' ... 'z': + case 'A' ... 'Z': + case '0' ... '9': + case '_': + case '-': + case '+': + case '.': + break; + + default: + return false; + } + } + + return true; +} +UNIGNORE_WARNINGS + +static void +kitty_notification(struct terminal *term, char *string) +{ + /* https://sw.kovidgoyal.net/kitty/desktop-notifications */ + + char *payload_raw = strchr(string, ';'); + if (payload_raw == NULL) + return; + + char *parameters = string; + *payload_raw = '\0'; + payload_raw++; + + char *id = NULL; /* The 'i' parameter */ + char *app_id = NULL; /* The 'f' parameter */ + char *icon_cache_id = NULL; /* The 'g' parameter */ + char *symbolic_icon = NULL; /* The 'n' parameter */ + char *category = NULL; /* The 't' parameter */ + char *sound_name = NULL; /* The 's' parameter */ + char *payload = NULL; + + bool focus = true; /* The 'a' parameter */ + bool report_activated = false; /* The 'a' parameter */ + bool report_closed = false; /* The 'c' parameter */ + bool done = true; /* The 'd' parameter */ + bool base64 = false; /* The 'e' parameter */ + + int32_t expire_time = -1; /* The 'w' parameter */ + + size_t payload_size; + enum { + PAYLOAD_TITLE, + PAYLOAD_BODY, + PAYLOAD_CLOSE, + PAYLOAD_ALIVE, + PAYLOAD_ICON, + PAYLOAD_BUTTON, + } payload_type = PAYLOAD_TITLE; /* The 'p' parameter */ + + enum notify_when when = NOTIFY_ALWAYS; + enum notify_urgency urgency = NOTIFY_URGENCY_NORMAL; + + bool have_a = false; + bool have_c = false; + bool have_o = false; + bool have_u = false; + bool have_w = false; + + char *ctx = NULL; + for (char *param = strtok_r(parameters, ":", &ctx); + param != NULL; + param = strtok_r(NULL, ":", &ctx)) + { + /* All parameters are on the form X=value, where X is always + exactly one character */ + if (param[0] == '\0' || param[1] != '=') + continue; + + char *value = ¶m[2]; + + switch (param[0]) { + case 'a': { + /* notification activation action: focus|report|-focus|-report */ + have_a = true; + char *a_ctx = NULL; + + for (const char *v = strtok_r(value, ",", &a_ctx); + v != NULL; + v = strtok_r(NULL, ",", &a_ctx)) + { + bool reverse = v[0] == '-'; + if (reverse) + v++; + + if (streq(v, "focus")) + focus = !reverse; + else if (streq(v, "report")) + report_activated = !reverse; + } + + break; + } + + case 'c': + if (value[0] == '1' && value[1] == '\0') + report_closed = true; + else if (value[0] == '0' && value[1] == '\0') + report_closed = false; + have_c = true; + break; + + case 'd': + /* done: 0|1 */ + if (value[0] == '0' && value[1] == '\0') + done = false; + else if (value[0] == '1' && value[1] == '\0') + done = true; + break; + + case 'e': + /* base64 (payload encoding): 0=utf8, 1=base64(utf8) */ + if (value[0] == '0' && value[1] == '\0') + base64 = false; + else if (value[0] == '1' && value[1] == '\0') + base64 = true; + break; + + case 'i': + /* id */ + if (verify_kitty_id_is_valid(value)) { + free(id); + id = xstrdup(value); + } else + LOG_WARN("OSC-99: ignoring invalid 'i' identifier"); + break; + + case 'p': + /* payload content: title|body */ + if (streq(value, "title")) + payload_type = PAYLOAD_TITLE; + else if (streq(value, "body")) + payload_type = PAYLOAD_BODY; + else if (streq(value, "close")) + payload_type = PAYLOAD_CLOSE; + else if (streq(value, "alive")) + payload_type = PAYLOAD_ALIVE; + else if (streq(value, "icon")) + payload_type = PAYLOAD_ICON; + else if (streq(value, "buttons")) + payload_type = PAYLOAD_BUTTON; + else if (streq(value, "?")) { + /* Query capabilities */ + + const char *reply_id = id != NULL ? id : "0"; + + const char *p_caps = "title,body,?,close,alive,icon,buttons"; + const char *a_caps = "focus,report"; + const char *u_caps = "0,1,2"; + + char when_caps[64]; + strcpy(when_caps, "unfocused"); + if (!term->conf->desktop_notifications.inhibit_when_focused) + strcat(when_caps, ",always"); + + const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; + + char reply[128]; + size_t n = xsnprintf( + reply, sizeof(reply), + "\033]99;i=%s:p=?;p=%s:a=%s:o=%s:u=%s:c=1:w=1:s=system,silent,error,warn,warning,info,question%s", + reply_id, p_caps, a_caps, when_caps, u_caps, terminator); + + xassert(n < sizeof(reply)); + term_to_slave(term, reply, n); + goto out; + } + break; + + case 'o': + /* honor when: always|unfocused|invisible */ + have_o = true; + if (streq(value, "always")) + when = NOTIFY_ALWAYS; + else if (streq(value, "unfocused")) + when = NOTIFY_UNFOCUSED; + else if (streq(value, "invisible")) + when = NOTIFY_INVISIBLE; + break; + + case 'u': + /* urgency: 0=low, 1=normal, 2=critical */ + have_u = true; + if (value[0] == '0' && value[1] == '\0') + urgency = NOTIFY_URGENCY_LOW; + else if (value[0] == '1' && value[1] == '\0') + urgency = NOTIFY_URGENCY_NORMAL; + else if (value[0] == '2' && value[1] == '\0') + urgency = NOTIFY_URGENCY_CRITICAL; + break; + + case 'w': { + /* Notification timeout */ + errno = 0; + char *end = NULL; + long timeout = strtol(value, &end, 10); + + if (errno == 0 && *end == '\0' && timeout <= INT32_MAX) { + expire_time = timeout; + have_w = true; + } + break; + } + + case 'f': { + /* App-name */ + char *decoded = base64_decode(value, NULL); + if (decoded != NULL) { + free(app_id); + app_id = decoded; + } + break; + } + + case 't': { + /* Type (category) */ + char *decoded = base64_decode(value, NULL); + if (decoded != NULL) { + if (category == NULL) + category = decoded; + else { + /* Append, comma separated */ + char *old_category = category; + category = xstrjoin3(old_category, ",", decoded); + free(decoded); + free(old_category); + } + } + break; + } + + case 's': { + /* Sound */ + char *decoded = base64_decode(value, NULL); + if (decoded != NULL) { + free(sound_name); + sound_name = decoded; + + const char *translated_name = NULL; + + if (streq(decoded, "error")) + translated_name = "dialog-error"; + else if (streq(decoded, "warn") || streq(decoded, "warning")) + translated_name = "dialog-warning"; + else if (streq(decoded, "info")) + translated_name = "dialog-information"; + else if (streq(decoded, "question")) + translated_name = "dialog-question"; + + if (translated_name != NULL) { + free(sound_name); + sound_name = xstrdup(translated_name); + } + } + break; + } + + case 'g': + /* graphical ID (see 'n' and 'p=icon') */ + free(icon_cache_id); + icon_cache_id = xstrdup(value); + break; + + case 'n': { + /* Symbolic icon name, may used with 'g' */ + + /* + * Sigh, protocol says 'n' can be used multiple times, and + * that the terminal picks the first one that it can + * resolve. + * + * We can't resolve any icons at all. So, enter + * heuristics... let's pick the *shortest* symbolic + * name. The idea is that icon *names* are typically + * shorter than .desktop names, and macOS bundle + * identifiers. + */ + char *maybe_new_symbolic_icon = base64_decode(value, NULL); + if (maybe_new_symbolic_icon == NULL) + break; + + if (symbolic_icon == NULL || + strlen(maybe_new_symbolic_icon) < strlen(symbolic_icon)) + { + free(symbolic_icon); + symbolic_icon = maybe_new_symbolic_icon; + + /* Translate OSC-99 "special" names */ + if (symbolic_icon != NULL) { + const char *translated_name = NULL; + + if (streq(symbolic_icon, "error")) + translated_name = "dialog-error"; + else if (streq(symbolic_icon, "warn") || + streq(symbolic_icon, "warning")) + translated_name = "dialog-warning"; + else if (streq(symbolic_icon, "info")) + translated_name = "dialog-information"; + else if (streq(symbolic_icon, "question")) + translated_name = "dialog-question"; + else if (streq(symbolic_icon, "help")) + translated_name = "system-help"; + else if (streq(symbolic_icon, "file-manager")) + translated_name = "system-file-manager"; + else if (streq(symbolic_icon, "system-monitor")) + translated_name = "utilities-system-monitor"; + else if (streq(symbolic_icon, "text-editor")) + translated_name = "text-editor"; + + if (translated_name != NULL) { + free(symbolic_icon); + symbolic_icon = xstrdup(translated_name); + } + } + } else { + free(maybe_new_symbolic_icon); + } + break; + } + } + } + + if (base64) { + payload = base64_decode(payload_raw, &payload_size); + if (payload == NULL) + goto out; + } else { + payload = xstrdup(payload_raw); + payload_size = strlen(payload); + } + + /* Append metadata to previous notification chunk */ + struct notification *notif = &term->kitty_notification; + + if (!((id == NULL && notif->id == NULL) || + (id != NULL && notif->id != NULL && streq(id, notif->id))) || + !notif->may_be_programatically_closed) /* Free:d notification has this as false... */ + { + /* ID mismatch, ignore previous notification state */ + notify_free(term, notif); + + notif->id = id; + notif->when = when; + notif->urgency = urgency; + notif->expire_time = expire_time; + notif->focus = focus; + notif->may_be_programatically_closed = true; + notif->report_activated = report_activated; + notif->report_closed = report_closed; + + id = NULL; /* Prevent double free */ + } + + if (have_a) { + notif->focus = focus; + notif->report_activated = report_activated; + } + + if (have_c) + notif->report_closed = report_closed; + + if (have_o) + notif->when = when; + if (have_u) + notif->urgency = urgency; + if (have_w) + notif->expire_time = expire_time; + + if (icon_cache_id != NULL) { + free(notif->icon_cache_id); + notif->icon_cache_id = icon_cache_id; + icon_cache_id = NULL; /* Prevent double free */ + } + + if (symbolic_icon != NULL) { + free(notif->icon_symbolic_name); + notif->icon_symbolic_name = symbolic_icon; + symbolic_icon = NULL; + } + + if (app_id != NULL) { + free(notif->app_id); + notif->app_id = app_id; + app_id = NULL; /* Prevent double free */ + } + + if (category != NULL) { + if (notif->category == NULL) { + notif->category = category; + category = NULL; /* Prevent double free */ + } else { + /* Append, comma separated */ + char *new_category = xstrjoin3(notif->category, ",", category); + free(notif->category); + notif->category = new_category; + } + } + + if (sound_name != NULL) { + notif->muted = streq(sound_name, "silent"); + + if (notif->muted || streq(sound_name, "system")) { + free(notif->sound_name); + notif->sound_name = NULL; + } else { + free(notif->sound_name); + notif->sound_name = sound_name; + sound_name = NULL; /* Prevent double free */ + } + } + + /* Handled chunked payload - append to existing metadata */ + switch (payload_type) { + case PAYLOAD_TITLE: + case PAYLOAD_BODY: { + char **ptr = payload_type == PAYLOAD_TITLE + ? ¬if->title + : ¬if->body; + + if (*ptr == NULL) { + *ptr = payload; + payload = NULL; + } else { + char *old = *ptr; + *ptr = xstrjoin(old, payload); + free(old); + } + break; + } + + case PAYLOAD_CLOSE: + case PAYLOAD_ALIVE: + /* Ignore payload */ + break; + + case PAYLOAD_ICON: + if (notif->icon_data == NULL) { + notif->icon_data = (uint8_t *)payload; + notif->icon_data_sz = payload_size; + payload = NULL; + } else { + notif->icon_data = xrealloc( + notif->icon_data, notif->icon_data_sz + payload_size); + memcpy(¬if->icon_data[notif->icon_data_sz], payload, payload_size); + notif->icon_data_sz += payload_size; + } + break; + + case PAYLOAD_BUTTON: { + char *ctx = NULL; + for (const char *button = strtok_r(payload, "\u2028", &ctx); + button != NULL; + button = strtok_r(NULL, "\u2028", &ctx)) + { + if (button[0] != '\0') { + tll_push_back(notif->actions, xstrdup(button)); + } + } + + break; + } + } + + if (done) { + /* Update icon cache, if necessary */ + if (notif->icon_cache_id != NULL && + (notif->icon_symbolic_name != NULL || notif->icon_data != NULL)) + { + notify_icon_del(term, notif->icon_cache_id); + notify_icon_add(term, notif->icon_cache_id, + notif->icon_symbolic_name, + notif->icon_data, notif->icon_data_sz); + + /* Don't need this anymore */ + free(notif->icon_symbolic_name); + free(notif->icon_data); + notif->icon_symbolic_name = NULL; + notif->icon_data = NULL; + notif->icon_data_sz = 0; + } + + if (payload_type == PAYLOAD_CLOSE) { + if (notif->id != NULL) + notify_close(term, notif->id); + } else if (payload_type == PAYLOAD_ALIVE) { + char *alive_ids = NULL; + + tll_foreach(term->active_notifications, it) { + /* TODO: check with kitty: use "0" for all + notifications with no ID? */ + + const char *item_id = it->item.id != NULL ? it->item.id : "0"; + + if (alive_ids == NULL) + alive_ids = xstrdup(item_id); + else { + char *old_alive_ids = alive_ids; + alive_ids = xstrjoin3(old_alive_ids, ",", item_id); + free(old_alive_ids); + } + } + + char *reply = xasprintf( + "\033]99;i=%s:p=alive;%s\033\\", + notif->id != NULL ? notif->id : "0", + alive_ids != NULL ? alive_ids : ""); + + term_to_slave(term, reply, strlen(reply)); + free(reply); + free(alive_ids); + } else { + /* + * Show notification. + * + * The checks for title|body is to handle notifications that + * only load icon data into the icon cache + */ + if (notif->title != NULL || notif->body != NULL) { + notify_notify(term, notif); + } + } + + notify_free(term, notif); + } + +out: + free(id); + free(app_id); + free(icon_cache_id); + free(symbolic_icon); + free(payload); + free(category); + free(sound_name); +} + +static void +kitty_text_size(struct terminal *term, char *string) +{ + char *text = strchr(string, ';'); + if (text == NULL) + return; + + char *parameters = string; + *text = '\0'; + text++; + + char32_t *wchars = ambstoc32(text); + if (wchars == NULL) + return; + + int forced_width = 0; + + char *ctx = NULL; + for (char *param = strtok_r(parameters, ":", &ctx); + param != NULL; + param = strtok_r(NULL, ":", &ctx)) + { + /* All parameters are on the form X=value, where X is always + exactly one character */ + if (param[0] == '\0' || param[1] != '=') + continue; + + char *value = ¶m[2]; + + switch (param[0]) { + case 'w': { + errno = 0; + char *end = NULL; + unsigned long w = strtoul(value, &end, 10); + + if (*end == '\0' && errno == 0 && w <= 7) { + forced_width = (int)w; + break; + } else + LOG_ERR("OSC-66: invalid 'w' value, ignoring"); + break; + } + + case 's': + case 'n': + case 'd': + case 'v': + LOG_WARN("OSC-66: unsupported: '%c' parameter, ignoring", param[0]); + break; + } + } + + const size_t len = c32len(wchars); + + if (forced_width == 0) { + /* + * w=0 means we split the text up as we'd normally do... Since + * we don't support any other parameters of the text-sizing + * protocol, that means we just process the string as if it + * has been printed without this OSC. + */ + for (size_t i = 0; i < len; i++) + term_process_and_print_non_ascii(term, wchars[i]); + free(wchars); + return; + } + + size_t max_cp_width = 0; + size_t all_cp_width = 0; + + for (size_t i = 0; i < len; i++) { + const size_t cp_width = c32width(wchars[i]); + all_cp_width += cp_width; + max_cp_width = max(max_cp_width, cp_width); + } + + size_t calculated_width = 0; + switch (term->conf->tweak.grapheme_width_method) { + case GRAPHEME_WIDTH_WCSWIDTH: calculated_width = all_cp_width; break; + case GRAPHEME_WIDTH_MAX: calculated_width = max_cp_width; break; + case GRAPHEME_WIDTH_DOUBLE: calculated_width = min(max_cp_width, 2); break; + } + + const size_t width = forced_width == 0 ? calculated_width : forced_width; + + LOG_DBG("len=%zu, forced=%d, calculated=%zu, using=%zu", + len, forced_width, calculated_width, width); + +#if 0 + if (len == 1 && calculated_width == forced_width) { + /* + * Optimization: if there's a single codepoint, and either + * w=0, or the 'w' matches the calculated width, print + * codepoint directly instead of creating a combining + * character. + */ + term_print(term, wchars[0], width); + free(wchars); + return; + } +#endif + + uint32_t key = composed_key_from_chars(wchars, len); + + const struct composed *composed = composed_lookup_without_collision( + term->composed, &key, wchars, len - 1, wchars[len - 1], forced_width); + + if (composed == NULL) { + struct composed *new_cc = xmalloc(sizeof(*new_cc)); + new_cc->chars = wchars; + new_cc->count = len; + new_cc->key = key; + new_cc->width = width; + new_cc->forced_width = forced_width; + + term->composed_count++; + composed_insert(&term->composed, new_cc); + composed = new_cc; + } else if (composed->width == width) { + free(wchars); + } + + term_print( + term, CELL_COMB_CHARS_LO + composed->key, + composed->forced_width > 0 ? composed->forced_width : composed->width, + false); +} + +void +osc_dispatch(struct terminal *term) +{ + unsigned param = 0; + int data_ofs = 0; + + for (size_t i = 0; i < term->vt.osc.idx; i++, data_ofs++) { + char c = term->vt.osc.data[i]; + + if (c == ';') { + data_ofs++; + break; + } + + if (!isdigit(c)) { + UNHANDLED(); + return; + } + + param *= 10; + param += c - '0'; + } + + LOG_DBG("OSC: %.*s (param = %d)", + (int)term->vt.osc.idx, term->vt.osc.data, param); + + char *string = (char *)&term->vt.osc.data[data_ofs]; + + switch (param) { + case 0: /* icon + title */ + term_set_window_title(term, string); + break; + + case 1: /* icon */ + break; + + case 2: /* title */ + term_set_window_title(term, string); + break; + + case 4: { + /* Set color */ + + string--; + if (*string != ';') + break; + + xassert(*string == ';'); + + for (const char *s_idx = strtok(string, ";"), *s_color = strtok(NULL, ";"); + s_idx != NULL && s_color != NULL; + s_idx = strtok(NULL, ";"), s_color = strtok(NULL, ";")) + { + /* Parse parameter */ + unsigned idx = 0; + for (; *s_idx != '\0'; s_idx++) { + char c = *s_idx; + idx *= 10; + idx += c - '0'; + } + + if (idx >= ALEN(term->colors.table)) { + LOG_WARN("invalid OSC 4 color index: %u", idx); + break; + } + + /* Client queried for current value */ + if (s_color[0] == '?' && s_color[1] == '\0') { + uint32_t color = term->colors.table[idx]; + uint8_t r = (color >> 16) & 0xff; + uint8_t g = (color >> 8) & 0xff; + uint8_t b = (color >> 0) & 0xff; + const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; + + char reply[32]; + size_t n = xsnprintf( + reply, sizeof(reply), + "\033]4;%u;rgb:%02hhx%02hhx/%02hhx%02hhx/%02hhx%02hhx%s", + idx, r, r, g, g, b, b, terminator); + term_to_slave(term, reply, n); + } + + else { + uint32_t color; + bool color_is_valid = s_color[0] == '#' || s_color[0] == '[' + ? parse_legacy_color(s_color, &color, NULL, NULL) + : parse_rgb(s_color, &color, NULL, NULL); + + if (!color_is_valid) + continue; + + LOG_DBG("change color definition for #%u from %06x to %06x", + idx, term->colors.table[idx], color); + + term->colors.table[idx] = color; + term_damage_color(term, COLOR_BASE256, idx); + } + } + + break; + } + + case 7: + /* Update terminal's understanding of PWD */ + osc_set_pwd(term, string); + break; + + case 8: + osc_uri(term, string); + break; + + case 9: { + /* iTerm2 Growl notifications */ + const char *sep = strchr(string, ';'); + if (sep != NULL) { + errno = 0; + char *end = NULL; + strtoul(string, &end, 10); + if (end == sep && errno == 0) { + /* Ignore ConEmu/Windows Terminal escape */ + break; + } + } + + osc_notify(term, string); + break; + } + + case 10: /* fg */ + case 11: /* bg */ + case 12: /* cursor */ + case 17: /* highlight (selection) fg */ + case 19: { /* highlight (selection) bg */ + /* Set default foreground/background/highlight-bg/highlight-fg color */ + + /* Client queried for current value */ + if (string[0] == '?' && string[1] == '\0') { + uint32_t color = param == 10 + ? term->colors.fg + : param == 11 + ? term->colors.bg + : param == 12 + ? term->colors.cursor_bg + : param == 17 + ? term->colors.selection_bg + : term->colors.selection_fg; + + uint8_t r = (color >> 16) & 0xff; + uint8_t g = (color >> 8) & 0xff; + uint8_t b = (color >> 0) & 0xff; + const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; + + /* + * Reply in XParseColor format + * E.g. for color 0xdcdccc we reply "\033]10;rgb:dc/dc/cc\033\\" + */ + char reply[32]; + size_t n = xsnprintf( + reply, sizeof(reply), + "\033]%u;rgb:%02hhx%02hhx/%02hhx%02hhx/%02hhx%02hhx%s", + param, r, r, g, g, b, b, terminator); + + term_to_slave(term, reply, n); + break; + } + + uint32_t color; + bool have_alpha = false; + uint16_t alpha = 0xffff; + + if (string[0] == '#' || string[0] == '[' + ? !parse_legacy_color(string, &color, &have_alpha, &alpha) + : !parse_rgb(string, &color, &have_alpha, &alpha)) + { + break; + } + + LOG_DBG("change color definition for %s to %06x", + param == 10 ? "foreground" : + param == 11 ? "background" : + param == 12 ? "cursor" : + param == 17 ? "selection background" : + "selection foreground", + color); + + switch (param) { + case 10: + term->colors.fg = color; + term_damage_color(term, COLOR_DEFAULT, 0); + break; + + case 11: + term->colors.bg = color; + if (!have_alpha) + alpha = term_theme_get(term)->alpha; + + const bool changed = term->colors.alpha != alpha; + term->colors.alpha = alpha; + + if (changed) { + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + } + + term_damage_color(term, COLOR_DEFAULT, 0); + term_damage_margins(term); + break; + + case 12: + term->colors.cursor_bg = 1u << 31 | color; + term_damage_cursor(term); + break; + + case 17: + term->colors.selection_bg = color; + break; + + case 19: + term->colors.selection_fg = color; + break; + } + + break; + } + + case 22: /* Set mouse cursor */ + term_set_user_mouse_cursor(term, string); + break; + + case 30: /* Set tab title */ + break; + + case 52: /* Copy to/from clipboard/primary */ + osc_selection(term, string); + break; + + case 66: /* text-size protocol (kitty) */ + kitty_text_size(term, string); + break; + + case 99: /* Kitty notifications */ + kitty_notification(term, string); + break; + + case 104: { + /* Reset Color Number 'c' (whole table if no parameter) */ + + const struct color_theme *theme = term_theme_get(term); + + if (string[0] == '\0') { + LOG_DBG("resetting all colors"); + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); + term_damage_view(term); + } + + else { + for (const char *s_idx = strtok(string, ";"); + s_idx != NULL; + s_idx = strtok(NULL, ";")) + { + unsigned idx = 0; + for (; *s_idx != '\0'; s_idx++) { + char c = *s_idx; + idx *= 10; + idx += c - '0'; + } + + if (idx >= ALEN(term->colors.table)) { + LOG_WARN("invalid OSC 104 color index: %u", idx); + continue; + } + + LOG_DBG("resetting color #%u", idx); + term->colors.table[idx] = theme->table[idx]; + term_damage_color(term, COLOR_BASE256, idx); + } + + } + break; + } + + case 105: /* Reset Special Color Number 'c' */ + break; + + case 110: /* Reset default text foreground color */ + LOG_DBG("resetting foreground color"); + + const struct color_theme *theme = term_theme_get(term); + term->colors.fg = theme->fg; + term_damage_color(term, COLOR_DEFAULT, 0); + break; + + case 111: { /* Reset default text background color */ + LOG_DBG("resetting background color"); + + const struct color_theme *theme = term_theme_get(term); + bool alpha_changed = term->colors.alpha != theme->alpha; + + term->colors.bg = theme->bg; + term->colors.alpha = theme->alpha; + + if (alpha_changed) { + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + } + + term_damage_color(term, COLOR_DEFAULT, 0); + term_damage_margins(term); + break; + } + + case 112: { + LOG_DBG("resetting cursor color"); + + const struct color_theme *theme = term_theme_get(term); + term->colors.cursor_fg = theme->cursor.text; + term->colors.cursor_bg = theme->cursor.cursor; + + if (term->conf->colors_dark.use_custom.cursor) { + term->colors.cursor_fg |= 1u << 31; + term->colors.cursor_bg |= 1u << 31; + } + + term_damage_cursor(term); + break; + } + + case 117: { + LOG_DBG("resetting selection background color"); + + const struct color_theme *theme = term_theme_get(term); + term->colors.selection_bg = theme->selection_bg; + break; + } + + case 119: { + LOG_DBG("resetting selection foreground color"); + + const struct color_theme *theme = term_theme_get(term); + term->colors.selection_fg = theme->selection_fg; + break; + } + + case 133: + /* + * Shell integration; see + * https://iterm2.com/documentation-escape-codes.html (Shell + * Integration/FinalTerm) + * + * [PROMPT]prompt% [COMMAND_START] ls -l + * [COMMAND_EXECUTED] + * -rw-r--r-- 1 user group 127 May 1 2016 filename + * [COMMAND_FINISHED] + */ + switch (string[0]) { + case 'A': + LOG_DBG("FTCS_PROMPT: %dx%d", + term->grid->cursor.point.row, + term->grid->cursor.point.col); + + term->grid->cur_row->shell_integration.prompt_marker = true; + break; + + case 'B': + LOG_DBG("FTCS_COMMAND_START"); + break; + + case 'C': + LOG_DBG("FTCS_COMMAND_EXECUTED: %dx%d", + term->grid->cursor.point.row, + term->grid->cursor.point.col); + term->grid->cur_row->shell_integration.cmd_start = term->grid->cursor.point.col; + break; + + case 'D': + LOG_DBG("FTCS_COMMAND_FINISHED: %dx%d", + term->grid->cursor.point.row, + term->grid->cursor.point.col); + term->grid->cur_row->shell_integration.cmd_end = term->grid->cursor.point.col; + break; + } + break; + + case 176: + if (string[0] == '?' && string[1] == '\0') { +#if 0 /* Disabled for now, see #1894 */ + const char *terminator = term->vt.osc.bel ? "\a" : "\033\\"; + char *reply = xasprintf( + "\033]176;%s%s", + term->app_id != NULL ? term->app_id : term->conf->app_id, + terminator); + + term_to_slave(term, reply, strlen(reply)); + free(reply); +#else + LOG_WARN("OSC-176 app-id query ignored"); +#endif + break; + } + + term_set_app_id(term, string); + break; + + case 555: + osc_flash(term); + break; + + case 777: { + /* + * OSC 777 is an URxvt generic escape used to send commands to + * perl extensions. The generic syntax is: \E]777;;ST + * + * We only recognize the 'notify' command, which is, if not + * well established, at least fairly well known. + */ + + char *param_brk = strchr(string, ';'); + if (param_brk == NULL) { + UNHANDLED(); + return; + } + + if (strncmp(string, "notify", param_brk - string) == 0) + osc_notify(term, param_brk + 1); + else + UNHANDLED(); + break; + } + + default: + UNHANDLED(); + break; + } +} + +bool +osc_ensure_size(struct terminal *term, size_t required_size) +{ + if (likely(required_size <= term->vt.osc.size)) + return true; + + const size_t pow2_max = ~(SIZE_MAX >> 1); + if (unlikely(required_size > pow2_max)) { + LOG_ERR("required OSC buffer size (%zu) exceeds limit (%zu)", + required_size, pow2_max); + return false; + } + + size_t new_size = max(term->vt.osc.size, 4096); + while (new_size < required_size) { + new_size <<= 1; + } + + uint8_t *new_data = realloc(term->vt.osc.data, new_size); + if (new_data == NULL) { + LOG_ERRNO("failed to increase size of OSC buffer"); + return false; + } + + LOG_DBG("resized OSC buffer: %zu", new_size); + term->vt.osc.data = new_data; + term->vt.osc.size = new_size; + return true; +} diff --git a/osc.h b/osc.h new file mode 100644 index 0000000..0820a8f --- /dev/null +++ b/osc.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include "terminal.h" + +bool osc_ensure_size(struct terminal *term, size_t required_size); +void osc_dispatch(struct terminal *term); diff --git a/pgo/full-current-session.sh b/pgo/full-current-session.sh new file mode 100755 index 0000000..363ee79 --- /dev/null +++ b/pgo/full-current-session.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +set -eux + +srcdir=$(realpath "${1}") +blddir=$(realpath "${2}") + +"${srcdir}"/pgo/full-inner.sh "${srcdir}" "${blddir}" diff --git a/pgo/full-headless-cage.sh b/pgo/full-headless-cage.sh new file mode 100755 index 0000000..50fc750 --- /dev/null +++ b/pgo/full-headless-cage.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +set -eux + +srcdir=$(realpath "${1}") +blddir=$(realpath "${2}") + +runtime_dir=$(mktemp -d) +trap "rm -rf '${runtime_dir}'" EXIT INT HUP TERM + +XDG_RUNTIME_DIR="${runtime_dir}" WLR_RENDERER=pixman WLR_BACKENDS=headless cage "${srcdir}"/pgo/full-inner.sh "${srcdir}" "${blddir}" + +# Cage's exit code doesn't reflect our script's exit code +[ -f "${blddir}"/pgo-ok ] || exit 1 diff --git a/pgo/full-headless-sway-inner.sh b/pgo/full-headless-sway-inner.sh new file mode 100755 index 0000000..dd612a2 --- /dev/null +++ b/pgo/full-headless-sway-inner.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +set -ux + +srcdir=$(realpath "${1}") +blddir=$(realpath "${2}") + +"${srcdir}"/pgo/full-inner.sh "${srcdir}" "${blddir}" +swaymsg exit diff --git a/pgo/full-headless-sway.sh b/pgo/full-headless-sway.sh new file mode 100755 index 0000000..8f6812b --- /dev/null +++ b/pgo/full-headless-sway.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +set -eux + +srcdir=$(realpath "${1}") +blddir=$(realpath "${2}") + +runtime_dir=$(mktemp -d) +sway_conf=$(mktemp) + +cleanup() { + rm -f "${sway_conf}" + rm -rf "${runtime_dir}" +} +trap cleanup EXIT INT HUP TERM + +# Generate a custom config that executes our generate-pgo-data script +> "${sway_conf}" echo "exec '${srcdir}'/pgo/full-headless-sway-inner.sh '${srcdir}' '${blddir}'" + +# Run Sway. full-headless-sway-inner.sh ends with a 'swaymsg exit' +XDG_RUNTIME_DIR="${runtime_dir}" WLR_RENDERER=pixman WLR_BACKENDS=headless sway -c "${sway_conf}" --unsupported-gpu + +# Sway's exit code doesn't reflect our script's exit code +[ -f "${blddir}"/pgo-ok ] || exit 1 diff --git a/pgo/full-inner.sh b/pgo/full-inner.sh new file mode 100755 index 0000000..c2205e5 --- /dev/null +++ b/pgo/full-inner.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +set -eux + +srcdir=$(realpath "${1}") +blddir=$(realpath "${2}") + +. "${srcdir}"/pgo/options + +pgo_data=$(mktemp) +trap "rm -f '${pgo_data}'" EXIT INT HUP TERM + +rm -f "${blddir}"/pgo-ok + +# To ensure profiling data is generated in the build directory +cd "${blddir}" + +"${blddir}"/utils/xtgettcap +"${blddir}"/footclient --version +"${blddir}"/foot \ + --config=/dev/null \ + --override tweak.grapheme-shaping=no \ + --term=xterm \ + sh -c " + set -eux + + '${srcdir}/scripts/generate-alt-random-writes.py' \ + ${script_options} \"${pgo_data}\" + + cat \"${pgo_data}\" + " +touch "${blddir}"/pgo-ok diff --git a/pgo/options b/pgo/options new file mode 100644 index 0000000..3fb821e --- /dev/null +++ b/pgo/options @@ -0,0 +1 @@ +script_options="--scroll --scroll-region --colors-regular --colors-bright --colors-256 --colors-rgb --attr-bold --attr-italic --attr-underline --sixel" diff --git a/pgo/partial.sh b/pgo/partial.sh new file mode 100755 index 0000000..6d6fdff --- /dev/null +++ b/pgo/partial.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +set -eux + +srcdir=$(realpath "${1}") +blddir=$(realpath "${2}") + +. "${srcdir}"/pgo/options + +pgo_data=$(mktemp) +trap "rm -f ${pgo_data}" EXIT INT HUP TERM + +rm -f "${blddir}"/pgo-ok + +"${srcdir}"/scripts/generate-alt-random-writes.py \ + --rows=67 \ + --cols=135 \ + ${script_options} \ + "${pgo_data}" + +# To ensure profiling data is generated in the build directory +cd "${blddir}" + +"${blddir}"/utils/xtgettcap +"${blddir}"/footclient --version +"${blddir}"/foot --version +"${blddir}"/pgo "${pgo_data}" + +touch "${blddir}"/pgo-ok diff --git a/pgo/pgo.c b/pgo/pgo.c new file mode 100644 index 0000000..96ddcce --- /dev/null +++ b/pgo/pgo.c @@ -0,0 +1,442 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "async.h" +#include "config.h" +#include "key-binding.h" +#include "reaper.h" +#include "sixel.h" +#include "user-notification.h" +#include "vt.h" + +extern bool fdm_ptmx(struct fdm *fdm, int fd, int events, void *data); + +static void +usage(const char *prog_name) +{ + printf( + "Usage: %s stimuli-file1 stimuli-file2 ... stimuli-fileN\n", + prog_name); +} + +enum async_write_status +async_write(int fd, const void *data, size_t len, size_t *idx) +{ + return ASYNC_WRITE_DONE; +} + +bool +fdm_add(struct fdm *fdm, int fd, int events, fdm_fd_handler_t handler, void *data) +{ + return true; +} + +bool +fdm_del(struct fdm *fdm, int fd) +{ + return true; +} + +bool +fdm_event_add(struct fdm *fdm, int fd, int events) +{ + return true; +} + +bool +fdm_event_del(struct fdm *fdm, int fd, int events) +{ + return true; +} + +bool +render_resize( + struct terminal *term, int width, int height, uint8_t resize_options) +{ + return true; +} + +void render_refresh(struct terminal *term) {} +void render_refresh_csd(struct terminal *term) {} +void render_refresh_title(struct terminal *term) {} +void render_refresh_app_id(struct terminal *term) {} +void render_refresh_icon(struct terminal *term) {} +void render_refresh_tab_bar(struct terminal *term) {} + +void render_overlay(struct terminal *term) {} + +void render_buffer_release_callback(struct buffer *buf, void *data) {} + +bool +render_xcursor_is_valid(const struct seat *seat, const char *cursor) +{ + return true; +} + +bool +render_xcursor_set(struct seat *seat, struct terminal *term, enum cursor_shape shape) +{ + return true; +} + +enum cursor_shape +xcursor_for_csd_border(struct terminal *term, int x, int y) +{ + return CURSOR_SHAPE_LEFT_PTR; +} + +struct wl_window * +wayl_win_init(struct terminal *term, const char *token) +{ + return NULL; +} + +void wayl_win_destroy(struct wl_window *win) {} +void wayl_win_alpha_changed(struct wl_window *win) {} +bool wayl_win_set_urgent(struct wl_window *win) { return true; } +bool wayl_win_ring_bell(const struct wl_window *win) { return true; } +bool wayl_fractional_scaling(const struct wayland *wayl) { return true; } + +pid_t +spawn(struct reaper *reaper, const char *cwd, char *const argv[], + int stdin_fd, int stdout_fd, int stderr_fd, + reaper_cb cb, void *cb_data, const char *xdg_activation_token) +{ + return 2; +} + +pid_t +slave_spawn( + int ptmx, int argc, const char *cwd, char *const *argv, char *const *envp, + const env_var_list_t *extra_env_vars, const char *term_env, + const char *conf_shell, bool login_shell, + const user_notifications_t *notifications) +{ + return 0; +} + +int +render_worker_thread(void *_ctx) +{ + return 0; +} + +bool +wayl_do_linear_blending(const struct wayland *wayl, const struct config *conf) +{ + return false; +} + +struct extraction_context * +extract_begin(enum selection_kind kind, bool strip_trailing_empty) +{ + return NULL; +} + +bool +extract_one( + const struct terminal *term, const struct row *row, const struct cell *cell, + int col, void *context) +{ + return true; +} + +bool +extract_finish(struct extraction_context *context, char **text, size_t *len) +{ + return true; +} + +void cmd_scrollback_up(struct terminal *term, int rows) {} +void cmd_scrollback_down(struct terminal *term, int rows) {} + +void ime_enable(struct seat *seat) {} +void ime_disable(struct seat *seat) {} +void ime_reset_preedit(struct seat *seat) {} + +bool +notify_notify(struct terminal *term, struct notification *notif) +{ + return true; +} + +void +notify_close(struct terminal *term, const char *id) +{ +} + +void +notify_free(struct terminal *term, struct notification *notif) +{ +} + +void +notify_icon_add(struct terminal *term, const char *id, + const char *symbolic_name, const uint8_t *data, + size_t data_sz) +{ +} + +void +notify_icon_del(struct terminal *term, const char *id) +{ +} + +void +notify_icon_free(struct notification_icon *icon) +{ +} + +void reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data) {} +void reaper_del(struct reaper *reaper, pid_t pid) {} + +void urls_reset(struct terminal *term) {} + +void shm_unref(struct buffer *buf) {} +void shm_chain_free(struct buffer_chain *chain) {} +enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain) { return SHM_BITS_8; } + +struct buffer_chain * +shm_chain_new( + struct wayland *wayl, bool scrollable, size_t pix_instances, + enum shm_bit_depth desired_bit_depth, + void (*release_cb)(struct buffer *buf, void *data), void *cb_data) +{ + return NULL; +} + + +void search_selection_cancelled(struct terminal *term) {} + +void get_current_modifiers(const struct seat *seat, + xkb_mod_mask_t *effective, + xkb_mod_mask_t *consumed, uint32_t key, + bool filter_locked) {} + +static struct key_binding_set kbd; +static bool kbd_initialized = false; + +struct key_binding_set * +key_binding_for( + struct key_binding_manager *mgr, const struct config *conf, + const struct seat *seat) +{ + return &kbd; +} + +void +key_binding_new_for_conf( + struct key_binding_manager *mgr, const struct wayland *wayl, + const struct config *conf) +{ + if (!kbd_initialized) { + kbd_initialized = true; + kbd = (struct key_binding_set){ + .key = tll_init(), + .search = tll_init(), + .url = tll_init(), + .mouse = tll_init(), + .selection_overrides = 0, + }; + } +} + +void +key_binding_unref(struct key_binding_manager *mgr, const struct config *conf) +{ +} + +int +main(int argc, const char *const *argv) +{ + if (argc < 2) { + usage(argv[0]); + return EXIT_FAILURE; + } + + const int row_count = 67; + const int col_count = 135; + const int grid_row_count = 16384; + + int lower_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (lower_fd < 0) + return EXIT_FAILURE; + + int upper_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (upper_fd < 0) { + close(lower_fd); + return EXIT_FAILURE; + } + + struct row **normal_rows = calloc(grid_row_count, sizeof(normal_rows[0])); + struct row **alt_rows = calloc(grid_row_count, sizeof(alt_rows[0])); + + for (int i = 0; i < grid_row_count; i++) { + normal_rows[i] = calloc(1, sizeof(*normal_rows[i])); + normal_rows[i]->cells = calloc(col_count, sizeof(normal_rows[i]->cells[0])); + alt_rows[i] = calloc(1, sizeof(*alt_rows[i])); + alt_rows[i]->cells = calloc(col_count, sizeof(alt_rows[i]->cells[0])); + } + + struct config conf = { + .tweak = { + .delayed_render_lower_ns = 500000, /* 0.5ms */ + .delayed_render_upper_ns = 16666666 / 2, /* half a frame period (60Hz) */ + }, + }; + + struct wayland wayl = { + .seats = tll_init(), + .monitors = tll_init(), + .terms = tll_init(), + }; + + struct terminal term = { + .conf = &conf, + .wl = &wayl, + .grid = &term.normal, + .normal = { + .num_rows = grid_row_count, + .num_cols = col_count, + .rows = normal_rows, + .cur_row = normal_rows[0], + }, + .alt = { + .num_rows = grid_row_count, + .num_cols = col_count, + .rows = alt_rows, + .cur_row = alt_rows[0], + }, + .scale = 1, + .width = col_count * 8, + .height = row_count * 15, + .cols = col_count, + .rows = row_count, + .cell_width = 8, + .cell_height = 15, + .scroll_region = { + .start = 0, + .end = row_count, + }, + .selection = { + .coords = { + .start = {-1, -1}, + .end = {-1, -1}, + }, + }, + .delayed_render_timer = { + .lower_fd = lower_fd, + .upper_fd = upper_fd + }, + .sixel = { + .palette_size = SIXEL_MAX_COLORS, + .max_width = SIXEL_MAX_WIDTH, + .max_height = SIXEL_MAX_HEIGHT, + }, + }; + + tll_push_back(wayl.terms, &term); + + int ret = EXIT_FAILURE; + + for (int i = 1; i < argc; i++) { + struct stat st; + if (stat(argv[i], &st) < 0) { + fprintf(stderr, "error: %s: failed to stat: %s\n", + argv[i], strerror(errno)); + goto out; + } + + uint8_t *data = malloc(st.st_size); + if (data == NULL) { + fprintf(stderr, "error: %s: failed to allocate buffer: %s\n", + argv[i], strerror(errno)); + goto out; + } + + int fd = open(argv[1], O_RDONLY); + if (fd < 0) { + fprintf(stderr, "error: %s: failed to open: %s\n", + argv[i], strerror(errno)); + goto out; + } + + ssize_t amount = read(fd, data, st.st_size); + if (amount != st.st_size) { + fprintf(stderr, "error: %s: failed to read: %s\n", + argv[i], strerror(errno)); + goto out; + } + + close(fd); + +#if defined(MEMFD_CREATE) + int mem_fd = memfd_create("foot-pgo-ptmx", MFD_CLOEXEC); +#elif defined(__FreeBSD__) + // memfd_create on FreeBSD 13 is SHM_ANON without sealing support + int mem_fd = shm_open(SHM_ANON, O_RDWR | O_CLOEXEC, 0600); +#else + char name[] = "/tmp/foot-pgo-ptmx-XXXXXX"; + int mem_fd = mkostemp(name, O_CLOEXEC); + unlink(name); +#endif + if (mem_fd < 0) { + fprintf(stderr, "error: failed to create memory FD\n"); + goto out; + } + + if (write(mem_fd, data, st.st_size) < 0) { + fprintf(stderr, "error: failed to write memory FD\n"); + close(mem_fd); + goto out; + } + + free(data); + + term.ptmx = mem_fd; + lseek(mem_fd, 0, SEEK_SET); + + printf("Feeding VT parser with %s (%lld bytes)\n", + argv[i], (long long)st.st_size); + + while (lseek(mem_fd, 0, SEEK_CUR) < st.st_size) { + if (!fdm_ptmx(NULL, -1, EPOLLIN, &term)) { + fprintf(stderr, "error: fdm_ptmx() failed\n"); + close(mem_fd); + goto out; + } + } + close(mem_fd); + } + + ret = EXIT_SUCCESS; + +out: + tll_free(wayl.terms); + + for (int i = 0; i < grid_row_count; i++) { + if (normal_rows[i] != NULL) + free(normal_rows[i]->cells); + free(normal_rows[i]); + + if (alt_rows[i] != NULL) + free(alt_rows[i]->cells); + free(alt_rows[i]); + } + + free(normal_rows); + free(alt_rows); + close(lower_fd); + close(upper_fd); + return ret; +} diff --git a/pgo/pgo.sh b/pgo/pgo.sh new file mode 100755 index 0000000..24597c8 --- /dev/null +++ b/pgo/pgo.sh @@ -0,0 +1,116 @@ +#!/bin/sh + +set -eu + +usage_and_die() { + echo "Usage: ${0} none|partial|full-current-session|full-headless-sway|full-headless-cage|[auto] [meson options]" + exit 1 +} + +[ ${#} -ge 3 ] || usage_and_die + +mode=${1} +srcdir=$(realpath "${2}") +blddir=$(realpath "${3}") +shift 3 + +# if [ -e "${blddir}" ]; then +# echo "error: ${blddir}: build directory already exists" +# exit 1 +# fi + +if [ ! -f "${srcdir}"/generate-version.sh ]; then + echo "error: ${srcdir}: does not appear to be a foot source directory" + exit 1 +fi + +compiler=other +do_pgo=no + +CFLAGS="${CFLAGS-} -O3" + +case $(${CC-cc} --version) in + *Free\ Software\ Foundation*) + compiler=gcc + do_pgo=yes + ;; + + *clang*) + compiler=clang + + if command -v llvm-profdata > /dev/null; then + do_pgo=yes + CFLAGS="${CFLAGS} -Wno-ignored-optimization-argument" + fi + ;; +esac + +case ${mode} in + partial|full-current-session|full-headless-sway|full-headless-cage) + ;; + + none) + do_pgo=no + ;; + + auto) + if [ -n "${WAYLAND_DISPLAY+x}" ]; then + mode=full-current-session + elif command -v sway > /dev/null; then + mode=full-headless-sway + elif command -v cage > /dev/null; then + mode=full-headless-cage + else + mode=partial + fi + ;; + + *) + usage_and_die + ;; +esac + +set -x + +# echo "source: ${srcdir}" +# echo "build: ${blddir}" +# echo "compiler: ${compiler}" +# echo "mode: ${mode}" +# echo "CFLAGS: ${CFLAGS}" + +export CFLAGS +export CCACHE_DISABLE=1 +meson setup --buildtype=release -Db_lto=true "${@}" "${blddir}" "${srcdir}" + +if [ ${do_pgo} = yes ]; then + find "${blddir}" \ + '(' \ + -name "*.gcda" -o \ + -name "*.profraw" -o \ + -name default.profdata \ + ')' \ + -delete + + meson configure "${blddir}" -Db_pgo=generate + ninja -C "${blddir}" + + # If fcft/tllist are subprojects, we need to ensure their tests + # have been executed, or we'll get "profile count data file not + # found" errors. + ninja -C "${blddir}" test + + # Run mode-dependent script to generate profiling data + export LLVM_PROFILE_FILE="${blddir}/default_%m.profraw" + "${srcdir}"/pgo/${mode}.sh "${srcdir}" "${blddir}" + + if [ ${compiler} = clang ]; then + llvm-profdata \ + merge \ + "${blddir}"/default_*.profraw \ + --output="${blddir}"/default.profdata + fi + + meson configure "${blddir}" -Db_pgo=use +fi + +ninja -C "${blddir}" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f5fc08a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[tool.pyright] +strict = ['scripts'] + +[tool.mypy] +files = '$MYPY_CONFIG_FILE_DIR/scripts' +strict = true + +[tool.codespell] +skip = 'pyproject.toml,./subprojects,./pkg,./src,./bld,foot.info,./unicode,./venv' +ignore-regex = 'terminfo capability `rin`|\* Simon Ser|\* \[zar\]\(https://codeberg.org/zar\)|iterm theme|iterm.toml|iterm/OneHalfDark.itermcolors' \ No newline at end of file diff --git a/quirks.c b/quirks.c new file mode 100644 index 0000000..67cb587 --- /dev/null +++ b/quirks.c @@ -0,0 +1,86 @@ +#include "quirks.h" + +#include +#include +#include + +#define LOG_MODULE "quirks" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "util.h" + +static bool +is_weston(void) +{ + static bool is_weston = false; + static bool initialized = false; + + if (!initialized) { + initialized = true; + is_weston = getenv("WESTON_CONFIG_FILE") != NULL; + if (is_weston) + LOG_WARN("applying wl_subsurface_set_desync() workaround for weston"); + } + + return is_weston; +} + +void +quirk_weston_subsurface_desync_on(struct wl_subsurface *sub) +{ + if (!is_weston()) + return; + + wl_subsurface_set_desync(sub); +} + +void +quirk_weston_subsurface_desync_off(struct wl_subsurface *sub) +{ + if (!is_weston()) + return; + + wl_subsurface_set_sync(sub); +} + +void +quirk_weston_csd_on(struct terminal *term) +{ + if (term->window->csd_mode != CSD_YES) + return; + if (term->window->is_fullscreen) + return; + + for (int i = 0; i < ALEN(term->window->csd.surface); i++) + quirk_weston_subsurface_desync_on(term->window->csd.surface[i].sub); +} + +void +quirk_weston_csd_off(struct terminal *term) +{ + if (term->window->csd_mode != CSD_YES) + return; + if (term->window->is_fullscreen) + return; + + for (int i = 0; i < ALEN(term->window->csd.surface); i++) + quirk_weston_subsurface_desync_off(term->window->csd.surface[i].sub); +} + +#if 0 +static bool +is_sway(void) +{ + static bool is_sway = false; + static bool initialized = false; + + if (!initialized) { + initialized = true; + is_sway = getenv("SWAYSOCK") != NULL; + if (is_sway) + LOG_WARN("applying wl_surface_damage_buffer() workaround for Sway"); + } + + return is_sway; +} +#endif diff --git a/quirks.h b/quirks.h new file mode 100644 index 0000000..e762bb3 --- /dev/null +++ b/quirks.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "terminal.h" + +/* + * On weston (8.0), synchronized subsurfaces aren't updated correctly. + + * They appear to render once, but after that, updates are + * sporadic. Sometimes they update, most of the time they don't. + * + * Adding explicit parent surface commits right after the subsurface + * commit doesn't help (and would be useless anyway, since it would + * defeat the purpose of having the subsurface synchronized in the + * first place). + */ +void quirk_weston_subsurface_desync_on(struct wl_subsurface *sub); +void quirk_weston_subsurface_desync_off(struct wl_subsurface *sub); + +/* Shortcuts to call desync_{on,off} on all CSD subsurfaces */ +void quirk_weston_csd_on(struct terminal *term); +void quirk_weston_csd_off(struct terminal *term); diff --git a/reaper.c b/reaper.c new file mode 100644 index 0000000..4abc785 --- /dev/null +++ b/reaper.c @@ -0,0 +1,120 @@ +#include "reaper.h" + +#include +#include +#include +#include +#include +#include + +#define LOG_MODULE "reaper" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +struct child { + pid_t pid; + reaper_cb cb; + void *cb_data; +}; + +struct reaper { + struct fdm *fdm; + tll(struct child) children; +}; + +static bool fdm_reap(struct fdm *fdm, int signo, void *data); + +struct reaper * +reaper_init(struct fdm *fdm) +{ + struct reaper *reaper = malloc(sizeof(*reaper)); + if (unlikely(reaper == NULL)) { + LOG_ERRNO("malloc() failed"); + return NULL; + } + + *reaper = (struct reaper){ + .fdm = fdm, + .children = tll_init(), + }; + + if (!fdm_signal_add(fdm, SIGCHLD, &fdm_reap, reaper)) + goto err; + + return reaper; + +err: + tll_free(reaper->children); + free(reaper); + return NULL; +} + +void +reaper_destroy(struct reaper *reaper) +{ + if (reaper == NULL) + return; + + fdm_signal_del(reaper->fdm, SIGCHLD); + tll_free(reaper->children); + free(reaper); +} + +void +reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data) +{ + LOG_DBG("adding pid=%d", pid); + tll_push_back( + reaper->children, + ((struct child){.pid = pid, .cb = cb, .cb_data = cb_data})); +} + +void +reaper_del(struct reaper *reaper, pid_t pid) +{ + tll_foreach(reaper->children, it) { + if (it->item.pid == pid) { + tll_remove(reaper->children, it); + break; + } + } +} + +static bool +fdm_reap(struct fdm *fdm, int signo, void *data) +{ + struct reaper *reaper = data; + + while (true) { + int status; + pid_t pid = waitpid(-1, &status, WNOHANG); + if (pid <= 0) + break; + + if (WIFEXITED(status)) + LOG_DBG("pid=%d: exited with status=%d", pid, WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + LOG_DBG("pid=%d: killed by signal=%d", pid, WTERMSIG(status)); + else + LOG_DBG("pid=%d: died of unknown resason", pid); + + tll_foreach(reaper->children, it) { + struct child *_child = &it->item; + + if (_child->pid != pid) + continue; + + /* Make sure we remove it *before* the callback, since it too + * may remove it */ + struct child child = it->item; + tll_remove(reaper->children, it); + + if (child.cb != NULL) + child.cb(reaper, child.pid, status, child.cb_data); + + break; + } + } + + return true; +} diff --git a/reaper.h b/reaper.h new file mode 100644 index 0000000..4416af0 --- /dev/null +++ b/reaper.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +#include "fdm.h" + +struct reaper; + +struct reaper *reaper_init(struct fdm *fdm); +void reaper_destroy(struct reaper *reaper); + +typedef void (*reaper_cb)( + struct reaper *reaper, pid_t pid, int status, void *data); + +void reaper_add(struct reaper *reaper, pid_t pid, reaper_cb cb, void *cb_data); +void reaper_del(struct reaper *reaper, pid_t pid); diff --git a/render.c b/render.c new file mode 100644 index 0000000..c8f1a4e --- /dev/null +++ b/render.c @@ -0,0 +1,6421 @@ +#include "render.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "macros.h" +#if HAS_INCLUDE() + #include + #define pthread_setname_np(thread, name) (pthread_set_name_np(thread, name), 0) +#elif defined(__NetBSD__) + #define pthread_setname_np(thread, name) pthread_setname_np(thread, "%s", (void *)name) +#endif + +#include +#include +#include +#include + +#include + +#define LOG_MODULE "render" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "box-drawing.h" +#include "char32.h" +#include "config.h" +#include "cursor-shape.h" +#include "grid.h" +#include "ime.h" +#include "quirks.h" +#include "search.h" +#include "selection.h" +#include "shm.h" +#include "sixel.h" +#include "srgb.h" +#include "url-mode.h" +#include "util.h" +#include "xmalloc.h" + +#define TIME_SCROLL_DAMAGE 0 + +struct renderer { + struct fdm *fdm; + struct wayland *wayl; +}; + +static struct { + size_t total; + size_t zero; /* commits presented in less than one frame interval */ + size_t one; /* commits presented in one frame interval */ + size_t two; /* commits presented in two or more frame intervals */ +} presentation_statistics = {0}; + +static void fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data); +static void render_tab_bar(struct terminal *term); +static void render_tab_overview(struct terminal *term); + +struct renderer * +render_init(struct fdm *fdm, struct wayland *wayl) +{ + struct renderer *renderer = malloc(sizeof(*renderer)); + if (unlikely(renderer == NULL)) { + LOG_ERRNO("malloc() failed"); + return NULL; + } + + *renderer = (struct renderer) { + .fdm = fdm, + .wayl = wayl, + }; + + if (!fdm_hook_add(fdm, &fdm_hook_refresh_pending_terminals, renderer, + FDM_HOOK_PRIORITY_NORMAL)) + { + LOG_ERR("failed to register FDM hook"); + free(renderer); + return NULL; + } + + return renderer; +} + +void +render_destroy(struct renderer *renderer) +{ + if (renderer == NULL) + return; + + fdm_hook_del(renderer->fdm, &fdm_hook_refresh_pending_terminals, + FDM_HOOK_PRIORITY_NORMAL); + + free(renderer); +} + +static void DESTRUCTOR +log_presentation_statistics(void) +{ + if (presentation_statistics.total == 0) + return; + + const size_t total = presentation_statistics.total; + LOG_INFO("presentation statistics: zero=%f%%, one=%f%%, two=%f%%", + 100. * presentation_statistics.zero / total, + 100. * presentation_statistics.one / total, + 100. * presentation_statistics.two / total); +} + +static void +sync_output(void *data, + struct wp_presentation_feedback *wp_presentation_feedback, + struct wl_output *output) +{ +} + +struct presentation_context { + struct terminal *term; + struct timeval input; + struct timeval commit; +}; + +static void +presented(void *data, + struct wp_presentation_feedback *wp_presentation_feedback, + uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec, + uint32_t refresh, uint32_t seq_hi, uint32_t seq_lo, uint32_t flags) +{ + struct presentation_context *ctx = data; + struct terminal *term = ctx->term; + const struct timeval *input = &ctx->input; + const struct timeval *commit = &ctx->commit; + + const struct timeval presented = { + .tv_sec = (uint64_t)tv_sec_hi << 32 | tv_sec_lo, + .tv_usec = tv_nsec / 1000, + }; + + bool use_input = (input->tv_sec > 0 || input->tv_usec > 0) && + timercmp(&presented, input, >); + char msg[1024]; + int chars = 0; + + if (use_input && timercmp(&presented, input, <)) + return; + else if (timercmp(&presented, commit, <)) + return; + + LOG_DBG("commit: %lu s %lu µs, presented: %lu s %lu µs", + commit->tv_sec, commit->tv_usec, presented.tv_sec, presented.tv_usec); + + if (use_input) { + struct timeval diff; + timersub(commit, input, &diff); + chars += snprintf( + &msg[chars], sizeof(msg) - chars, + "input - %llu µs -> ", (unsigned long long)diff.tv_usec); + } + + struct timeval diff; + timersub(&presented, commit, &diff); + chars += snprintf( + &msg[chars], sizeof(msg) - chars, + "commit - %llu µs -> ", (unsigned long long)diff.tv_usec); + + if (use_input) { + xassert(timercmp(&presented, input, >)); + timersub(&presented, input, &diff); + } else { + xassert(timercmp(&presented, commit, >)); + timersub(&presented, commit, &diff); + } + + chars += snprintf( + &msg[chars], sizeof(msg) - chars, + "presented (total: %llu µs)", (unsigned long long)diff.tv_usec); + + unsigned frame_count = 0; + if (tll_length(term->window->on_outputs) > 0) { + const struct monitor *mon = tll_front(term->window->on_outputs); + frame_count = (diff.tv_sec * 1000000. + diff.tv_usec) / (1000000. / mon->refresh); + } + + presentation_statistics.total++; + if (frame_count >= 2) + presentation_statistics.two++; + else if (frame_count >= 1) + presentation_statistics.one++; + else + presentation_statistics.zero++; + +#define _log_fmt "%s (more than %u frames)" + + if (frame_count >= 2) + LOG_ERR(_log_fmt, msg, frame_count); + else if (frame_count >= 1) + LOG_WARN(_log_fmt, msg, frame_count); + else + LOG_INFO(_log_fmt, msg, frame_count); + +#undef _log_fmt + + wp_presentation_feedback_destroy(wp_presentation_feedback); + free(ctx); +} + +static void +discarded(void *data, struct wp_presentation_feedback *wp_presentation_feedback) +{ + struct presentation_context *ctx = data; + wp_presentation_feedback_destroy(wp_presentation_feedback); + free(ctx); +} + +static const struct wp_presentation_feedback_listener presentation_feedback_listener = { + .sync_output = &sync_output, + .presented = &presented, + .discarded = &discarded, +}; + +static struct fcft_font * +attrs_to_font(const struct terminal *term, const struct attributes *attrs) +{ + int idx = attrs->italic << 1 | attrs->bold; + return term->fonts[idx]; +} + +static pixman_color_t +color_hex_to_pixman_srgb(uint32_t color, uint16_t alpha) +{ + return (pixman_color_t){ + .alpha = alpha, /* Consider alpha linear already? */ + .red = srgb_decode_8_to_16((color >> 16) & 0xff), + .green = srgb_decode_8_to_16((color >> 8) & 0xff), + .blue = srgb_decode_8_to_16((color >> 0) & 0xff), + }; +} + +static inline pixman_color_t +color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha, bool srgb) +{ + pixman_color_t ret; + + if (srgb) + ret = color_hex_to_pixman_srgb(color, alpha); + else { + ret = (pixman_color_t){ + .red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)), + .green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)), + .blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)), + .alpha = alpha, + }; + } + + ret.red = (uint32_t)ret.red * alpha / 0xffff; + ret.green = (uint32_t)ret.green * alpha / 0xffff; + ret.blue = (uint32_t)ret.blue * alpha / 0xffff; + + return ret; +} + +static inline pixman_color_t +color_hex_to_pixman(uint32_t color, bool srgb) +{ + /* Count on the compiler optimizing this */ + return color_hex_to_pixman_with_alpha(color, 0xffff, srgb); +} + +static inline int i_lerp(int from, int to, float t) { + return from + (to - from) * t; +} + +static inline uint32_t +color_blend_towards(uint32_t from, uint32_t to, float amount) +{ + if (unlikely(amount == 0)) + return from; + float t = 1 - 1/amount; + + uint32_t alpha = from & 0xff000000; + uint8_t r = i_lerp((from>>16)&0xff, (to>>16)&0xff, t); + uint8_t g = i_lerp((from>>8)&0xff, (to>>8)&0xff, t); + uint8_t b = i_lerp((from>>0)&0xff, (to>>0)&0xff, t); + + return alpha | (r<<16) | (g<<8) | (b<<0); +} + +static inline uint32_t +color_dim(const struct terminal *term, uint32_t color) +{ + const struct config *conf = term->conf; + const uint8_t custom_dim = conf->colors_dark.use_custom.dim; + + if (unlikely(custom_dim != 0)) { + for (size_t i = 0; i < 8; i++) { + if (((custom_dim >> i) & 1) == 0) + continue; + + if (term->colors.table[0 + i] == color) { + /* "Regular" color, return the corresponding "dim" */ + return conf->colors_dark.dim[i]; + } + + else if (term->colors.table[8 + i] == color) { + /* "Bright" color, return the corresponding "regular" */ + return term->colors.table[i]; + } + } + } + + const struct color_theme *theme = term_theme_get(term); + + return color_blend_towards( + color, + theme->dim_blend_towards == DIM_BLEND_TOWARDS_BLACK ? 0x00000000 : 0x00ffffff, + conf->dim.amount); +} + +static inline uint32_t +color_brighten(const struct terminal *term, uint32_t color) +{ + /* + * First try to match the color against the base 8 colors. If we + * find a match, return the corresponding bright color. + */ + if (term->conf->bold_in_bright.palette_based) { + for (size_t i = 0; i < 8; i++) { + if (term->colors.table[i] == color) + return term->colors.table[i + 8]; + } + return color; + } + + return color_blend_towards(color, 0x00ffffff, term->conf->bold_in_bright.amount); +} + +static void +draw_hollow_block(const struct terminal *term, pixman_image_t *pix, + const pixman_color_t *color, int x, int y, int cell_cols) +{ + const int scale = (int)roundf(term->scale); + const int width = min(min(scale, term->cell_width), term->cell_height); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, 4, + (pixman_rectangle16_t []){ + {x, y, cell_cols * term->cell_width, width}, /* top */ + {x, y, width, term->cell_height}, /* left */ + {x + cell_cols * term->cell_width - width, y, width, term->cell_height}, /* right */ + {x, y + term->cell_height - width, cell_cols * term->cell_width, width}, /* bottom */ + }); +} + +static void +draw_beam_cursor(const struct terminal *term, pixman_image_t *pix, + const struct fcft_font *font, + const pixman_color_t *color, int x, int y) +{ + int baseline = y + term->font_baseline - term->fonts[0]->ascent; + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, + 1, &(pixman_rectangle16_t){ + x, baseline, + term_pt_or_px_as_pixels(term, &term->conf->cursor.beam_thickness), + term->fonts[0]->ascent + term->fonts[0]->descent}); +} + +static int +underline_offset(const struct terminal *term, const struct fcft_font *font) +{ + return term->font_baseline - + (term->conf->use_custom_underline_offset + ? -term_pt_or_px_as_pixels(term, &term->conf->underline_offset) + : font->underline.position); +} + +static void +draw_underline_cursor(const struct terminal *term, pixman_image_t *pix, + const struct fcft_font *font, + const pixman_color_t *color, int x, int y, int cols) +{ + int thickness = term->conf->cursor.underline_thickness.px >= 0 + ? term_pt_or_px_as_pixels( + term, &term->conf->cursor.underline_thickness) + : font->underline.thickness; + + /* Make sure the line isn't positioned below the cell */ + const int y_ofs = min(underline_offset(term, font) + thickness, + term->cell_height - thickness); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, + 1, &(pixman_rectangle16_t){ + x, y + y_ofs, cols * term->cell_width, thickness}); +} + +static void +draw_underline(const struct terminal *term, pixman_image_t *pix, + const struct fcft_font *font, + const pixman_color_t *color, int x, int y, int cols) +{ + const int thickness = term->conf->underline_thickness.px >= 0 + ? term_pt_or_px_as_pixels( + term, &term->conf->underline_thickness) + : font->underline.thickness; + + /* Make sure the line isn't positioned below the cell */ + const int y_ofs = min(underline_offset(term, font), + term->cell_height - thickness); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, + 1, &(pixman_rectangle16_t){ + x, y + y_ofs, cols * term->cell_width, thickness}); +} + +static void +draw_styled_underline(const struct terminal *term, pixman_image_t *pix, + const struct fcft_font *font, + const pixman_color_t *color, + enum underline_style style, int x, int y, int cols) +{ + xassert(style != UNDERLINE_NONE); + + if (style == UNDERLINE_SINGLE) { + draw_underline(term, pix, font, color, x, y, cols); + return; + } + + const int thickness = term->conf->underline_thickness.px >= 0 + ? term_pt_or_px_as_pixels( + term, &term->conf->underline_thickness) + : font->underline.thickness; + + int y_ofs; + + /* Make sure the line isn't positioned below the cell */ + switch (style) { + case UNDERLINE_DOUBLE: + case UNDERLINE_CURLY: + y_ofs = min(underline_offset(term, font), + term->cell_height - thickness * 3); + break; + + case UNDERLINE_DASHED: + case UNDERLINE_DOTTED: + y_ofs = min(underline_offset(term, font), + term->cell_height - thickness); + break; + + case UNDERLINE_NONE: + case UNDERLINE_SINGLE: + default: + BUG("unexpected underline style: %d", (int)style); + return; + } + + const int ceil_w = cols * term->cell_width; + + switch (style) { + case UNDERLINE_DOUBLE: { + const pixman_rectangle16_t rects[] = { + {x, y + y_ofs, ceil_w, thickness}, + {x, y + y_ofs + thickness * 2, ceil_w, thickness}}; + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 2, rects); + break; + } + + case UNDERLINE_DASHED: { + const int ceil_w = cols * term->cell_width; + const int dash_w = ceil_w / 3 + (ceil_w % 3 > 0); + const pixman_rectangle16_t rects[] = { + {x, y + y_ofs, dash_w, thickness}, + {x + dash_w * 2, y + y_ofs, dash_w, thickness}, + }; + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, 2, rects); + break; + } + + case UNDERLINE_DOTTED: { + /* Number of dots per cell */ + int per_cell = (term->cell_width / thickness) / 2; + if (per_cell == 0) + per_cell = 1; + + xassert(per_cell >= 1); + + /* Spacing between dots; start with the same width as the dots + themselves, then widen them if necessary, to consume unused + pixels */ + int spacing[per_cell]; + for (int i = 0; i < per_cell; i++) + spacing[i] = thickness; + + /* Pixels remaining at the end of the cell */ + int remaining = term->cell_width - (per_cell * 2) * thickness; + + /* Spread out the left-over pixels across the spacing between + the dots */ + for (int i = 0; remaining > 0; i = (i + 1) % per_cell, remaining--) + spacing[i]++; + + xassert(remaining <= 0); + + pixman_rectangle16_t rects[per_cell]; + int dot_x = x; + for (int i = 0; i < per_cell; i++) { + rects[i] = (pixman_rectangle16_t){ + dot_x, y + y_ofs, thickness, thickness + }; + + dot_x += thickness + spacing[i]; + } + + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, per_cell, rects); + break; + } + + case UNDERLINE_CURLY: { + const int top = y + y_ofs; + const int bot = top + thickness * 3; + const int half_x = x + ceil_w / 2.0, full_x = x + ceil_w; + + const double bt_2 = (bot - top) * (bot - top); + const double th_2 = thickness * thickness; + const double hx_2 = ceil_w * ceil_w / 4.0; + const int th = round(sqrt(th_2 + (th_2 * bt_2 / hx_2)) / 2.); + + #define I(x) pixman_int_to_fixed(x) + const pixman_trapezoid_t traps[] = { +#if 0 /* characters sit within the "dips" of the curlies */ + { + I(top), I(bot), + {{I(x), I(top + th)}, {I(half_x), I(bot + th)}}, + {{I(x), I(top - th)}, {I(half_x), I(bot - th)}}, + }, + { + I(top), I(bot), + {{I(half_x), I(bot - th)}, {I(full_x), I(top - th)}}, + {{I(half_x), I(bot + th)}, {I(full_x), I(top + th)}}, + } +#else /* characters sit on top of the curlies */ + { + I(top), I(bot), + {{I(x), I(bot - th)}, {I(half_x), I(top - th)}}, + {{I(x), I(bot + th)}, {I(half_x), I(top + th)}}, + }, + { + I(top), I(bot), + {{I(half_x), I(top + th)}, {I(full_x), I(bot + th)}}, + {{I(half_x), I(top - th)}, {I(full_x), I(bot - th)}}, + } +#endif + }; + + pixman_image_t *fill = pixman_image_create_solid_fill(color); + pixman_composite_trapezoids( + PIXMAN_OP_OVER, fill, pix, PIXMAN_a8, 0, 0, 0, 0, + sizeof(traps) / sizeof(traps[0]), traps); + + pixman_image_unref(fill); + break; + } + + case UNDERLINE_NONE: + case UNDERLINE_SINGLE: + BUG("underline styles not supposed to be handled here"); + break; + } +} + +static void +draw_strikeout(const struct terminal *term, pixman_image_t *pix, + const struct fcft_font *font, + const pixman_color_t *color, int x, int y, int cols) +{ + const int thickness = term->conf->strikeout_thickness.px >= 0 + ? term_pt_or_px_as_pixels( + term, &term->conf->strikeout_thickness) + : font->strikeout.thickness; + + /* Try to center custom strikeout */ + const int position = term->conf->strikeout_thickness.px >= 0 + ? font->strikeout.position - round(font->strikeout.thickness / 2.) + round(thickness / 2.) + : font->strikeout.position; + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, color, + 1, &(pixman_rectangle16_t){ + x, y + term->font_baseline - position, + cols * term->cell_width, thickness}); +} + +static void +cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, + const pixman_color_t *fg, const pixman_color_t *bg, + pixman_color_t *cursor_color, pixman_color_t *text_color, + bool gamma_correct) +{ + if (term->colors.cursor_bg >> 31) + *cursor_color = color_hex_to_pixman(term->colors.cursor_bg, gamma_correct); + else + *cursor_color = *fg; + + if (term->colors.cursor_fg >> 31) + *text_color = color_hex_to_pixman(term->colors.cursor_fg, gamma_correct); + else { + xassert(bg->alpha == 0xffff); + *text_color = *bg; + } + + if (text_color->red == cursor_color->red && + text_color->green == cursor_color->green && + text_color->blue == cursor_color->blue) + { + *text_color = color_hex_to_pixman(term->colors.bg, gamma_correct); + *cursor_color = color_hex_to_pixman(term->colors.fg, gamma_correct); + } +} + +static void +draw_cursor(const struct terminal *term, const struct cell *cell, + const struct fcft_font *font, pixman_image_t *pix, pixman_color_t *fg, + const pixman_color_t *bg, int x, int y, int cols) +{ + pixman_color_t cursor_color; + pixman_color_t text_color; + cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color, + wayl_do_linear_blending(term->wl, term->conf)); + + if (unlikely(!term->kbd_focus)) { + switch (term->conf->cursor.unfocused_style) { + case CURSOR_UNFOCUSED_UNCHANGED: + break; + + case CURSOR_UNFOCUSED_HOLLOW: + draw_hollow_block(term, pix, &cursor_color, x, y, cols); + return; + + case CURSOR_UNFOCUSED_NONE: + return; + } + } + + switch (term->cursor_style) { + case CURSOR_BLOCK: + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON) || + !term->kbd_focus) + { + *fg = text_color; + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, &cursor_color, 1, + &(pixman_rectangle16_t){x, y, cols * term->cell_width, term->cell_height}); + } + break; + + case CURSOR_BEAM: + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON || + !term->kbd_focus)) + { + draw_beam_cursor(term, pix, font, &cursor_color, x, y); + } + break; + + case CURSOR_UNDERLINE: + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON || + !term->kbd_focus)) + { + draw_underline_cursor(term, pix, font, &cursor_color, x, y, cols); + } + break; + + case CURSOR_HOLLOW: + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) + draw_hollow_block(term, pix, &cursor_color, x, y, cols); + break; + } +} + +static int +render_cell(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, struct row *row, int row_no, int col, + bool has_cursor) +{ + struct cell *cell = &row->cells[col]; + if (cell->attrs.clean) + return 0; + + cell->attrs.clean = 1; + cell->attrs.confined = true; + + int width = term->cell_width; + int height = term->cell_height; + const int x = term->margins.left + col * width; + const int y = term->margins.top + row_no * height; + + uint32_t _fg = 0; + uint32_t _bg = 0; + + uint16_t alpha = 0xffff; + const bool is_selected = cell->attrs.selected; + + /* Use cell specific color, if set, otherwise the default colors (possible reversed) */ + switch (cell->attrs.fg_src) { + case COLOR_RGB: + _fg = cell->attrs.fg; + break; + + case COLOR_BASE16: + case COLOR_BASE256: + xassert(cell->attrs.fg < ALEN(term->colors.table)); + _fg = term->colors.table[cell->attrs.fg]; + break; + + case COLOR_DEFAULT: + _fg = term->reverse ? term->colors.bg : term->colors.fg; + break; + } + + switch (cell->attrs.bg_src) { + case COLOR_RGB: + _bg = cell->attrs.bg; + break; + + case COLOR_BASE16: + case COLOR_BASE256: + xassert(cell->attrs.bg < ALEN(term->colors.table)); + _bg = term->colors.table[cell->attrs.bg]; + break; + + case COLOR_DEFAULT: + _bg = term->reverse ? term->colors.fg : term->colors.bg; + break; + } + + if (unlikely(is_selected)) { + const uint32_t cell_fg = _fg; + const uint32_t cell_bg = _bg; + + const bool custom_fg = term->colors.selection_fg >> 24 == 0; + const bool custom_bg = term->colors.selection_bg >> 24 == 0; + const bool custom_both = custom_fg && custom_bg; + + if (custom_both) { + _fg = term->colors.selection_fg; + _bg = term->colors.selection_bg; + } else if (custom_bg) { + _bg = term->colors.selection_bg; + _fg = cell->attrs.reverse ? cell_bg : cell_fg; + } else if (custom_fg) { + _fg = term->colors.selection_fg; + _bg = cell->attrs.reverse ? cell_fg : cell_bg; + } else { + _bg = cell_fg; + _fg = cell_bg; + } + + if (unlikely(_fg == _bg)) { + /* Invert bg when selected/highlighted text has same fg/bg */ + _bg = ~_bg; + alpha = 0xffff; + } + + } else { + if (unlikely(cell->attrs.reverse)) { + uint32_t swap = _fg; + _fg = _bg; + _bg = swap; + } + + else if (!term->window->is_fullscreen && term->colors.alpha != 0xffff) { + switch (term->conf->colors_dark.alpha_mode) { + case ALPHA_MODE_DEFAULT: { + if (cell->attrs.bg_src == COLOR_DEFAULT) { + alpha = term->colors.alpha; + } + break; + } + + case ALPHA_MODE_MATCHING: { + if (cell->attrs.bg_src == COLOR_DEFAULT || + ((cell->attrs.bg_src == COLOR_BASE16 || + cell->attrs.bg_src == COLOR_BASE256) && + term->colors.table[cell->attrs.bg] == term->colors.bg) || + (cell->attrs.bg_src == COLOR_RGB && + cell->attrs.bg == term->colors.bg)) + { + alpha = term->colors.alpha; + } + break; + } + + case ALPHA_MODE_ALL: { + alpha = term->colors.alpha; + break; + } + } + } else { + /* + * Note: disable transparency when fullscreened. + * + * This is because the wayland protocol mandates no screen + * content is shown behind the fullscreened window. + * + * The _intent_ of the specification is that a black (or + * other static color) should be used as background. + * + * There's a bit of gray area however, and some + * compositors have chosen to interpret the specification + * in a way that allows wallpapers to be seen through a + * fullscreen window. + * + * Given that a) the intent of the specification, and b) + * we don't know what the compositor will do, we simply + * disable transparency while in fullscreen. + * + * To see why, consider what happens if we keep our + * transparency. For example, if the background color is + * white, and alpha is 0.5, then the window will be drawn + * in a shade of gray while fullscreened. + * + * See + * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/116 + * for a discussion on whether transparent, fullscreen + * windows should be allowed in some way or not. + * + * NOTE: if changing this, also update render_margin() + */ + xassert(alpha == 0xffff); + } + } + + if (cell->attrs.dim) + _fg = color_dim(term, _fg); + if (term->conf->bold_in_bright.enabled && cell->attrs.bold) + _fg = color_brighten(term, _fg); + + if (cell->attrs.blink && term->blink.state == BLINK_OFF) + _fg = color_blend_towards(_fg, 0x00000000, term->conf->dim.amount); + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); + + struct fcft_font *font = attrs_to_font(term, &cell->attrs); + const struct composed *composed = NULL; + const struct fcft_grapheme *grapheme = NULL; + const struct fcft_glyph *single = NULL; + const struct fcft_glyph **glyphs = NULL; + unsigned glyph_count = 0; + + char32_t base = cell->wc; + int cell_cols = 1; + + if (base != 0) { + if (unlikely( + /* Classic box drawings */ + (base >= GLYPH_BOX_DRAWING_FIRST && + base <= GLYPH_BOX_DRAWING_LAST) || + + /* Braille */ + (base >= GLYPH_BRAILLE_FIRST && + base <= GLYPH_BRAILLE_LAST) || + + /* + * Unicode 13 "Symbols for Legacy Computing" + * sub-ranges below. + * + * Note, the full range is U+1FB00 - U+1FBF9 + */ + (base >= GLYPH_LEGACY_FIRST && + base <= GLYPH_LEGACY_LAST) || + + /* + * Unicode 16 "Symbols for Legacy Computing Supplement" + * + * Note, the full range is U+1CC00 - U+1CEAF + */ + (base >= GLYPH_OCTANTS_FIRST && + base <= GLYPH_OCTANTS_LAST)) && + + likely(!term->conf->box_drawings_uses_font_glyphs)) + { + struct fcft_glyph ***arr; + size_t count; + size_t idx; + + if (base >= GLYPH_LEGACY_FIRST) { + arr = &term->custom_glyphs.legacy; + count = GLYPH_LEGACY_COUNT; + idx = base - GLYPH_LEGACY_FIRST; + } else if (base >= GLYPH_OCTANTS_FIRST) { + arr = &term->custom_glyphs.octants; + count = GLYPH_OCTANTS_COUNT; + idx = base - GLYPH_OCTANTS_FIRST; + } else if (base >= GLYPH_BRAILLE_FIRST) { + arr = &term->custom_glyphs.braille; + count = GLYPH_BRAILLE_COUNT; + idx = base - GLYPH_BRAILLE_FIRST; + } else { + arr = &term->custom_glyphs.box_drawing; + count = GLYPH_BOX_DRAWING_COUNT; + idx = base - GLYPH_BOX_DRAWING_FIRST; + } + + if (unlikely(*arr == NULL)) + *arr = xcalloc(count, sizeof((*arr)[0])); + + if (likely((*arr)[idx] != NULL)) + single = (*arr)[idx]; + else { + mtx_lock(&term->render.workers.lock); + + /* Other thread may have instantiated it while we + * acquired the lock */ + single = (*arr)[idx]; + if (likely(single == NULL)) + single = (*arr)[idx] = box_drawing(term, base); + mtx_unlock(&term->render.workers.lock); + } + + if (single != NULL) { + glyph_count = 1; + glyphs = &single; + cell_cols = single->cols; + } + } + + else if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) + { + composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); + base = composed->chars[0]; + + if (term->conf->can_shape_grapheme && term->conf->tweak.grapheme_shaping) { + grapheme = fcft_rasterize_grapheme_utf32( + font, composed->count, composed->chars, term->font_subpixel); + } + + if (grapheme != NULL) { + const int forced_width = composed->forced_width; + + cell_cols = forced_width > 0 ? forced_width : composed->width; + + composed = NULL; + glyphs = grapheme->glyphs; + glyph_count = grapheme->count; + + if (forced_width > 0) + glyph_count = min(glyph_count, forced_width); + } + } + + if (single == NULL && grapheme == NULL) { + if (unlikely(base >= CELL_SPACER)) { + glyph_count = 0; + cell_cols = 1; + } else { + xassert(base != 0); + single = fcft_rasterize_char_utf32(font, base, term->font_subpixel); + if (single == NULL) { + glyph_count = 0; + cell_cols = 1; + } else { + glyph_count = 1; + glyphs = &single; + + const size_t forced_width = composed != NULL ? composed->forced_width : 0; + cell_cols = forced_width > 0 ? forced_width : single->cols; + } + } + } + } + + assert(glyph_count == 0 || glyphs != NULL); + + const int cols_left = term->cols - col; + cell_cols = max(1, min(cell_cols, cols_left)); + + /* + * Determine cells that will bleed into their right neighbor and remember + * them for cleanup in the next frame. + */ + int render_width = cell_cols * width; + if (term->conf->tweak.overflowing_glyphs && + glyph_count > 0 && + cols_left > cell_cols) + { + int glyph_width = 0, advance = 0; + for (size_t i = 0; i < glyph_count; i++) { + glyph_width = max(glyph_width, + advance + glyphs[i]->x + glyphs[i]->width); + advance += glyphs[i]->advance.x; + } + + if (glyph_width > render_width) { + render_width = min(glyph_width, render_width + width); + + for (int i = 0; i < cell_cols; i++) + row->cells[col + i].attrs.confined = false; + } + } + + pixman_region32_t clip; + pixman_region32_init_rect( + &clip, x, y, + render_width, term->cell_height); + pixman_image_set_clip_region32(pix, &clip); + + if (damage != NULL) { + pixman_region32_union_rect( + damage, damage, x, y, render_width, term->cell_height); + } + + pixman_region32_fini(&clip); + + /* Background */ + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, pix, &bg, 1, + &(pixman_rectangle16_t){x, y, cell_cols * width, height}); + + if (cell->attrs.blink && term->blink.fd < 0) { + /* TODO: use a custom lock for this? */ + mtx_lock(&term->render.workers.lock); + term_arm_blink_timer(term); + mtx_unlock(&term->render.workers.lock); + } + + if (unlikely(has_cursor && term->cursor_style == CURSOR_BLOCK && term->kbd_focus)) { + const pixman_color_t bg_without_alpha = color_hex_to_pixman(_bg, gamma_correct); + draw_cursor(term, cell, font, pix, &fg, &bg_without_alpha, x, y, cell_cols); + } + + if (cell->wc == 0 || cell->wc >= CELL_SPACER || cell->wc == U'\t' || + (unlikely(cell->attrs.conceal) && !is_selected)) + { + goto draw_cursor; + } + + pixman_image_t *clr_pix = pixman_image_create_solid_fill(&fg); + + int pen_x = x; + for (unsigned i = 0; i < glyph_count; i++) { + const int letter_x_ofs = i == 0 ? term->font_x_ofs : 0; + + const struct fcft_glyph *glyph = glyphs[i]; + if (glyph == NULL) + continue; + + int g_x = glyph->x; + int g_y = glyph->y; + + if (i > 0 && glyph->x >= 0 && cell_cols == 1) + g_x -= term->cell_width; + + if (unlikely(glyph->is_color_glyph)) { + /* Glyph surface is a pre-rendered image (typically a color emoji...) */ + if (!(cell->attrs.blink && term->blink.state == BLINK_OFF)) { + pixman_image_composite32( + PIXMAN_OP_OVER, glyph->pix, NULL, pix, 0, 0, 0, 0, + pen_x + letter_x_ofs + g_x, y + term->font_baseline - g_y, + glyph->width, glyph->height); + } + } else { + pixman_image_composite32( + PIXMAN_OP_OVER, clr_pix, glyph->pix, pix, 0, 0, 0, 0, + pen_x + letter_x_ofs + g_x, y + term->font_baseline - g_y, + glyph->width, glyph->height); + + /* Combining characters */ + if (composed != NULL) { + assert(glyph_count == 1); + + for (size_t j = 1; j < composed->count; j++) { + const struct fcft_glyph *g = fcft_rasterize_char_utf32( + font, composed->chars[j], term->font_subpixel); + + if (g == NULL) + continue; + + /* + * Fonts _should_ assume the pen position is now + * *after* the base glyph, and thus use negative + * offsets for combining glyphs. + * + * Not all fonts behave like this however, and we + * try to accommodate both variants. + * + * Since we haven't moved our pen position yet, we + * add a full cell width to the offset (or two, in + * case of double-width characters). + * + * If the font does *not* use negative offsets, + * we'd normally use an offset of 0. However, to + * somewhat deal with double-width glyphs we use + * an offset of *one* cell. + */ + int x_ofs = cell_cols == 1 + ? g->x < 0 + ? cell_cols * term->cell_width + : (cell_cols - 1) * term->cell_width + : 0; + + if (cell_cols > 1) + pen_x += term->cell_width; + + pixman_image_composite32( + PIXMAN_OP_OVER, clr_pix, g->pix, pix, 0, 0, 0, 0, + /* Some fonts use a negative offset, while others use a + * "normal" offset */ + pen_x + letter_x_ofs + x_ofs + g->x, + y + term->font_baseline - g->y, g->width, g->height); + } + } + } + + pen_x += cell_cols > 1 ? term->cell_width : glyph->advance.x; + } + + pixman_image_unref(clr_pix); + + /* Underline */ + if (cell->attrs.underline) { + pixman_color_t underline_color = fg; + enum underline_style underline_style = UNDERLINE_SINGLE; + + /* Check if cell has a styled underline. This lookup is fairly + expensive... */ + if (row->extra != NULL) { + for (int i = 0; i < row->extra->underline_ranges.count; i++) { + const struct row_range *range = &row->extra->underline_ranges.v[i]; + + if (range->start > col) + break; + + if (range->start <= col && col <= range->end) { + switch (range->underline.color_src) { + case COLOR_BASE256: + underline_color = color_hex_to_pixman( + term->colors.table[range->underline.color], gamma_correct); + break; + + case COLOR_RGB: + underline_color = + color_hex_to_pixman(range->underline.color, gamma_correct); + break; + + case COLOR_DEFAULT: + break; + + case COLOR_BASE16: + BUG("underline color can't be base-16"); + break; + } + + underline_style = range->underline.style; + break; + } + } + } + + draw_styled_underline( + term, pix, font, &underline_color, underline_style, x, y, cell_cols); + + } + + if (cell->attrs.strikethrough) + draw_strikeout(term, pix, font, &fg, x, y, cell_cols); + + if (unlikely(cell->attrs.url && term->conf->url.style != UNDERLINE_NONE)) { + pixman_color_t url_color = color_hex_to_pixman( + term->conf->colors_dark.use_custom.url + ? term->conf->colors_dark.url + : term->colors.table[3], + gamma_correct); + + draw_styled_underline( + term, pix, font, &url_color, term->conf->url.style, + x, y, cell_cols); + } + +draw_cursor: + if (has_cursor && (term->cursor_style != CURSOR_BLOCK || !term->kbd_focus)) { + const pixman_color_t bg_without_alpha = color_hex_to_pixman(_bg, gamma_correct); + draw_cursor(term, cell, font, pix, &fg, &bg_without_alpha, x, y, cell_cols); + } + + pixman_image_set_clip_region32(pix, NULL); + return cell_cols; +} + +static void +render_row(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, struct row *row, + int row_no, int cursor_col) +{ + for (int col = term->cols - 1; col >= 0; col--) + render_cell(term, pix, damage, row, row_no, col, cursor_col == col); +} + +static void +render_urgency(struct terminal *term, struct buffer *buf) +{ + uint32_t red = term->colors.table[1]; + pixman_color_t bg = color_hex_to_pixman( + red, wayl_do_linear_blending(term->wl, term->conf)); + + int width = min(min(term->margins.left, term->margins.right), + min(term->margins.top, term->margins.bottom)); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &bg, 4, + (pixman_rectangle16_t[]){ + /* Top */ + {0, 0, term->width, width}, + + /* Bottom */ + {0, term->height - width, term->width, width}, + + /* Left */ + {0, width, width, term->height - 2 * width}, + + /* Right */ + {term->width - width, width, width, term->height - 2 * width}, + }); +} + +static void +render_margin(struct terminal *term, struct buffer *buf, + int start_line, int end_line, bool apply_damage) +{ + /* Fill area outside the cell grid with the default background color */ + const int rmargin = term->width - term->margins.right; + const int bmargin = term->height - term->margins.bottom; + const int line_count = end_line - start_line; + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + const uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; + uint16_t alpha = term->colors.alpha; + + if (term->window->is_fullscreen) { + /* Disable alpha in fullscreen - see render_cell() for details */ + alpha = 0xffff; + } + + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &bg, 4, + (pixman_rectangle16_t[]){ + /* Top */ + {0, 0, term->width, term->margins.top}, + + /* Bottom */ + {0, bmargin, term->width, term->margins.bottom}, + + /* Left */ + {0, term->margins.top + start_line * term->cell_height, + term->margins.left, line_count * term->cell_height}, + + /* Right */ + {rmargin, term->margins.top + start_line * term->cell_height, + term->margins.right, line_count * term->cell_height}, + }); + + if (term->render.urgency) + render_urgency(term, buf); + + /* Ensure the updated regions are copied to the next frame's + * buffer when we're double buffering */ + pixman_region32_union_rect( + &buf->dirty[0], &buf->dirty[0], 0, 0, term->width, term->margins.top); + pixman_region32_union_rect( + &buf->dirty[0], &buf->dirty[0], 0, bmargin, term->width, term->margins.bottom); + pixman_region32_union_rect( + &buf->dirty[0], &buf->dirty[0], 0, 0, term->margins.left, term->height); + pixman_region32_union_rect( + &buf->dirty[0], &buf->dirty[0], + rmargin, 0, term->margins.right, term->height); + + if (apply_damage) { + /* Top */ + wl_surface_damage_buffer( + term->window->surface.surf, 0, 0, term->width, term->margins.top); + + /* Bottom */ + wl_surface_damage_buffer( + term->window->surface.surf, 0, bmargin, term->width, term->margins.bottom); + + /* Left */ + wl_surface_damage_buffer( + term->window->surface.surf, + 0, term->margins.top + start_line * term->cell_height, + term->margins.left, line_count * term->cell_height); + + /* Right */ + wl_surface_damage_buffer( + term->window->surface.surf, + rmargin, term->margins.top + start_line * term->cell_height, + term->margins.right, line_count * term->cell_height); + } +} + +static void +grid_render_scroll(struct terminal *term, struct buffer *buf, + const struct damage *dmg) +{ + LOG_DBG( + "damage: SCROLL: %d-%d by %d lines", + dmg->region.start, dmg->region.end, dmg->lines); + + const int region_size = dmg->region.end - dmg->region.start; + + if (dmg->lines >= region_size) { + /* The entire scroll region will be scrolled out (i.e. replaced) */ + return; + } + + const int height = (region_size - dmg->lines) * term->cell_height; + xassert(height > 0); + +#if TIME_SCROLL_DAMAGE + struct timespec start_time; + clock_gettime(CLOCK_MONOTONIC, &start_time); +#endif + + int dst_y = term->margins.top + (dmg->region.start + 0) * term->cell_height; + int src_y = term->margins.top + (dmg->region.start + dmg->lines) * term->cell_height; + + /* + * SHM scrolling can be *much* faster, but it depends on how many + * lines we're scrolling, and how much repairing we need to do. + * + * In short, scrolling a *large* number of rows is faster with a + * memmove, while scrolling a *small* number of lines is faster + * with SHM scrolling. + * + * However, since we need to restore the scrolling regions when + * SHM scrolling, we also need to take this into account. + * + * Finally, we also have to restore the window margins, and this + * is a *huge* performance hit when scrolling a large number of + * lines (in addition to the sloweness of SHM scrolling as + * method). + * + * So, we need to figure out when to SHM scroll, and when to + * memmove. + * + * For now, assume that the both methods perform roughly the same, + * given an equal number of bytes to move/allocate, and use the + * method that results in the least amount of bytes to touch. + * + * Since number of lines directly translates to bytes, we can + * simply count lines. + * + * SHM scrolling needs to first "move" (punch hole + allocate) + * dmg->lines number of lines, and then we need to restore + * the bottom scroll region. + * + * If the total number of lines is less than half the screen - use + * SHM. Otherwise use memmove. + */ + bool try_shm_scroll = + shm_can_scroll(buf) && ( + dmg->lines + + dmg->region.start + + (term->rows - dmg->region.end)) < term->rows / 2; + + bool did_shm_scroll = false; + + //try_shm_scroll = false; + //try_shm_scroll = true; + + if (try_shm_scroll) { + did_shm_scroll = shm_scroll( + buf, dmg->lines * term->cell_height, + term->margins.top, dmg->region.start * term->cell_height, + term->margins.bottom, (term->rows - dmg->region.end) * term->cell_height); + } + + if (did_shm_scroll) { + /* Restore margins */ + render_margin( + term, buf, dmg->region.end - dmg->lines, term->rows, false); + } else { + /* Fallback for when we either cannot do SHM scrolling, or it failed */ + uint8_t *raw = buf->data; + memmove(raw + dst_y * buf->stride, + raw + src_y * buf->stride, + height * buf->stride); + } + +#if TIME_SCROLL_DAMAGE + struct timespec end_time; + clock_gettime(CLOCK_MONOTONIC, &end_time); + + struct timespec memmove_time; + timespec_sub(&end_time, &start_time, &memmove_time); + LOG_INFO("scrolled %dKB (%d lines) using %s in %lds %ldns", + height * buf->stride / 1024, dmg->lines, + did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove", + (long)memmove_time.tv_sec, memmove_time.tv_nsec); +#endif + + wl_surface_damage_buffer( + term->window->surface.surf, term->margins.left, dst_y, + term->width - term->margins.left - term->margins.right, height); + + /* + * TODO: remove this if re-enabling scroll damage when re-applying + * last frame's damage (see reapply_old_damage() + */ + pixman_region32_union_rect( + &buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); +} + +static void +grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, + const struct damage *dmg) +{ + LOG_DBG( + "damage: SCROLL REVERSE: %d-%d by %d lines", + dmg->region.start, dmg->region.end, dmg->lines); + + const int region_size = dmg->region.end - dmg->region.start; + + if (dmg->lines >= region_size) { + /* The entire scroll region will be scrolled out (i.e. replaced) */ + return; + } + + const int height = (region_size - dmg->lines) * term->cell_height; + xassert(height > 0); + +#if TIME_SCROLL_DAMAGE + struct timespec start_time; + clock_gettime(CLOCK_MONOTONIC, &start_time); +#endif + + int src_y = term->margins.top + (dmg->region.start + 0) * term->cell_height; + int dst_y = term->margins.top + (dmg->region.start + dmg->lines) * term->cell_height; + + bool try_shm_scroll = + shm_can_scroll(buf) && ( + dmg->lines + + dmg->region.start + + (term->rows - dmg->region.end)) < term->rows / 2; + + bool did_shm_scroll = false; + + if (try_shm_scroll) { + did_shm_scroll = shm_scroll( + buf, -dmg->lines * term->cell_height, + term->margins.top, dmg->region.start * term->cell_height, + term->margins.bottom, (term->rows - dmg->region.end) * term->cell_height); + } + + if (did_shm_scroll) { + /* Restore margins */ + render_margin( + term, buf, dmg->region.start, dmg->region.start + dmg->lines, false); + } else { + /* Fallback for when we either cannot do SHM scrolling, or it failed */ + uint8_t *raw = buf->data; + memmove(raw + dst_y * buf->stride, + raw + src_y * buf->stride, + height * buf->stride); + } + +#if TIME_SCROLL_DAMAGE + struct timespec end_time; + clock_gettime(CLOCK_MONOTONIC, &end_time); + + struct timespec memmove_time; + timespec_sub(&end_time, &start_time, &memmove_time); + LOG_INFO("scrolled REVERSE %dKB (%d lines) using %s in %lds %ldns", + height * buf->stride / 1024, dmg->lines, + did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove", + (long)memmove_time.tv_sec, memmove_time.tv_nsec); +#endif + + wl_surface_damage_buffer( + term->window->surface.surf, term->margins.left, dst_y, + term->width - term->margins.left - term->margins.right, height); + + /* + * TODO: remove this if re-enabling scroll damage when re-applying + * last frame's damage (see reapply_old_damage() + */ + pixman_region32_union_rect( + &buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); +} + +static void +render_sixel_chunk(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, const struct sixel *sixel, + int term_start_row, int img_start_row, int count) +{ + /* Translate row/column to x/y pixel values */ + const int x = term->margins.left + sixel->pos.col * term->cell_width; + const int y = term->margins.top + term_start_row * term->cell_height; + + /* Width/height, in pixels - and don't touch the window margins */ + const int width = max( + 0, + min(sixel->width, + term->width - x - term->margins.right)); + const int height = max( + 0, + min( + min(count * term->cell_height, /* 'count' number of rows */ + sixel->height - img_start_row * term->cell_height), /* What remains of the sixel */ + term->height - y - term->margins.bottom)); + + /* Verify we're not stepping outside the grid */ + xassert(x >= term->margins.left); + xassert(y >= term->margins.top); + xassert(width == 0 || x + width <= term->width - term->margins.right); + xassert(height == 0 || y + height <= term->height - term->margins.bottom); + + //LOG_DBG("sixel chunk: %dx%d %dx%d", x, y, width, height); + + pixman_image_composite32( + sixel->opaque ? PIXMAN_OP_SRC : PIXMAN_OP_OVER, + sixel->pix, + NULL, + pix, + 0, img_start_row * term->cell_height, + 0, 0, + x, y, + width, height); + + if (damage != NULL) + pixman_region32_union_rect(damage, damage, x, y, width, height); +} + +static void +render_sixel(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, const struct coord *cursor, + const struct sixel *sixel) +{ + xassert(sixel->pix != NULL); + xassert(sixel->width >= 0); + xassert(sixel->height >= 0); + + const int view_end = (term->grid->view + term->rows - 1) & (term->grid->num_rows - 1); + const bool last_row_needs_erase = sixel->height % term->cell_height != 0; + const bool last_col_needs_erase = sixel->width % term->cell_width != 0; + + int chunk_img_start = -1; /* Image-relative start row of chunk */ + int chunk_term_start = -1; /* Viewport relative start row of chunk */ + int chunk_row_count = 0; /* Number of rows to emit */ + +#define maybe_emit_sixel_chunk_then_reset() \ + if (chunk_row_count != 0) { \ + render_sixel_chunk( \ + term, pix, damage, sixel, \ + chunk_term_start, chunk_img_start, chunk_row_count); \ + chunk_term_start = chunk_img_start = -1; \ + chunk_row_count = 0; \ + } + + /* + * Iterate all sixel rows: + * + * - ignore rows that aren't visible on-screen + * - ignore rows that aren't dirty (they have already been rendered) + * - chunk consecutive dirty rows into a 'chunk' + * - emit (render) chunk as soon as a row isn't visible, or is clean + * - emit final chunk after we've iterated all rows + * + * The purpose of this is to reduce the amount of pixels that + * needs to be composited and marked as damaged for the + * compositor. + * + * Since we do CPU based composition, rendering is a slow and + * heavy task for foot, and thus it is important to not re-render + * things unnecessarily. + */ + + for (int _abs_row_no = sixel->pos.row; + _abs_row_no < sixel->pos.row + sixel->rows; + _abs_row_no++) + { + const int abs_row_no = _abs_row_no & (term->grid->num_rows - 1); + const int term_row_no = + (abs_row_no - term->grid->view + term->grid->num_rows) & + (term->grid->num_rows - 1); + + /* Check if row is in the visible viewport */ + if (view_end >= term->grid->view) { + /* Not wrapped */ + if (!(abs_row_no >= term->grid->view && abs_row_no <= view_end)) { + /* Not visible */ + maybe_emit_sixel_chunk_then_reset(); + continue; + } + } else { + /* Wrapped */ + if (!(abs_row_no >= term->grid->view || abs_row_no <= view_end)) { + /* Not visible */ + maybe_emit_sixel_chunk_then_reset(); + continue; + } + } + + /* Is the row dirty? */ + struct row *row = term->grid->rows[abs_row_no]; + xassert(row != NULL); /* Should be visible */ + + if (!row->dirty) { + maybe_emit_sixel_chunk_then_reset(); + continue; + } + + int cursor_col = cursor->row == term_row_no ? cursor->col : -1; + + /* + * If image contains transparent parts, render all (dirty) + * cells beneath it. + * + * If image is opaque, loop cells and set their 'clean' bit, + * to prevent the grid rendered from overwriting the sixel + * + * If the last sixel row only partially covers the cell row, + * 'erase' the cell by rendering them. + * + * In all cases, do *not* clear the 'dirty' bit on the row, to + * ensure the regular renderer includes them in the damage + * rect. + */ + if (!sixel->opaque) { + /* TODO: multithreading */ + render_row(term, pix, damage, row, term_row_no, cursor_col); + } else { + for (int col = sixel->pos.col; + col < min(sixel->pos.col + sixel->cols, term->cols); + col++) + { + struct cell *cell = &row->cells[col]; + + if (!cell->attrs.clean) { + bool last_row = abs_row_no == sixel->pos.row + sixel->rows - 1; + bool last_col = col == sixel->pos.col + sixel->cols - 1; + + if ((last_row_needs_erase && last_row) || + (last_col_needs_erase && last_col)) + { + render_cell(term, pix, damage, row, term_row_no, col, cursor_col == col); + } else { + cell->attrs.clean = 1; + cell->attrs.confined = 1; + } + } + } + } + + if (chunk_term_start == -1) { + xassert(chunk_img_start == -1); + chunk_term_start = term_row_no; + chunk_img_start = _abs_row_no - sixel->pos.row; + chunk_row_count = 1; + } else + chunk_row_count++; + } + + maybe_emit_sixel_chunk_then_reset(); +#undef maybe_emit_sixel_chunk_then_reset +} + +static void +render_sixel_images(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, const struct coord *cursor) +{ + if (likely(tll_length(term->grid->sixel_images)) == 0) + return; + + const int scrollback_end + = (term->grid->offset + term->rows) & (term->grid->num_rows - 1); + + const int view_start + = (term->grid->view + - scrollback_end + + term->grid->num_rows) & (term->grid->num_rows - 1); + + const int view_end = view_start + term->rows - 1; + + //LOG_DBG("SIXELS: %zu images, view=%d-%d", + // tll_length(term->grid->sixel_images), view_start, view_end); + + tll_foreach(term->grid->sixel_images, it) { + const struct sixel *six = &it->item; + const int start + = (six->pos.row + - scrollback_end + + term->grid->num_rows) & (term->grid->num_rows - 1); + const int end = start + six->rows - 1; + + //LOG_DBG(" sixel: %d-%d", start, end); + if (start > view_end) { + /* Sixel starts after view ends, no need to try to render it */ + continue; + } else if (end < view_start) { + /* Image ends before view starts. Since the image list is + * sorted, we can safely stop here */ + break; + } + + sixel_sync_cache(term, &it->item); + render_sixel(term, pix, damage, cursor, &it->item); + } +} + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED +static void +render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, + struct buffer *buf) +{ + if (likely(seat->ime.preedit.cells == NULL)) + return; + + if (unlikely(term->is_searching)) + return; + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + + /* Adjust cursor position to viewport */ + struct coord cursor; + cursor = term->grid->cursor.point; + cursor.row += term->grid->offset; + cursor.row -= term->grid->view; + cursor.row &= term->grid->num_rows - 1; + + if (cursor.row < 0 || cursor.row >= term->rows) + return; + + int cells_needed = seat->ime.preedit.count; + + if (seat->ime.preedit.cursor.start == cells_needed && + seat->ime.preedit.cursor.end == cells_needed) + { + /* Cursor will be drawn *after* the pre-edit string, i.e. in + * the cell *after*. This means we need to copy, and dirty, + * one extra cell from the original grid, or we'll leave + * trailing "cursors" after us if the user deletes text while + * pre-editing */ + cells_needed++; + } + + int row_idx = cursor.row; + int col_idx = cursor.col; + int ime_ofs = 0; /* Offset into pre-edit string to start rendering at */ + + int cells_left = term->cols - cursor.col; + int cells_used = min(cells_needed, term->cols); + + /* Adjust start of pre-edit text to the left if string doesn't fit on row */ + if (cells_left < cells_used) + col_idx -= cells_used - cells_left; + + if (cells_needed > cells_used) { + int start = seat->ime.preedit.cursor.start; + int end = seat->ime.preedit.cursor.end; + + if (start == end) { + /* Ensure *end* of pre-edit string is visible */ + ime_ofs = cells_needed - cells_used; + } else { + /* Ensure the *beginning* of the cursor-area is visible */ + ime_ofs = start; + + /* Display as much as possible of the pre-edit string */ + if (cells_needed - ime_ofs < cells_used) + ime_ofs = cells_needed - cells_used; + } + + /* Make sure we don't start in the middle of a character */ + while (ime_ofs < cells_needed && + seat->ime.preedit.cells[ime_ofs].wc >= CELL_SPACER) + { + ime_ofs++; + } + } + + xassert(col_idx >= 0); + xassert(col_idx < term->cols); + + struct row *row = grid_row_in_view(term->grid, row_idx); + + /* Don't start pre-edit text in the middle of a double-width character */ + while (col_idx > 0 && row->cells[col_idx].wc >= CELL_SPACER) { + cells_used++; + col_idx--; + } + + /* + * Copy original content (render_cell() reads cell data directly + * from grid), and mark all cells as dirty. This ensures they are + * re-rendered when the pre-edit text is modified or removed. + */ + struct cell *real_cells = xmalloc(cells_used * sizeof(real_cells[0])); + for (int i = 0; i < cells_used; i++) { + xassert(col_idx + i < term->cols); + real_cells[i] = row->cells[col_idx + i]; + real_cells[i].attrs.clean = 0; + } + row->dirty = true; + + /* Render pre-edit text */ + xassert(seat->ime.preedit.cells[ime_ofs].wc < CELL_SPACER); + for (int i = 0, idx = ime_ofs; idx < seat->ime.preedit.count; i++, idx++) { + const struct cell *cell = &seat->ime.preedit.cells[idx]; + + if (cell->wc >= CELL_SPACER) + continue; + + int width = max(1, c32width(cell->wc)); + if (col_idx + i + width > term->cols) + break; + + row->cells[col_idx + i] = *cell; + render_cell(term, buf->pix[0], NULL, row, row_idx, col_idx + i, false); + } + + int start = seat->ime.preedit.cursor.start - ime_ofs; + int end = seat->ime.preedit.cursor.end - ime_ofs; + + if (!seat->ime.preedit.cursor.hidden) { + const struct cell *start_cell = &seat->ime.preedit.cells[0]; + + pixman_color_t fg = color_hex_to_pixman(term->colors.fg, gamma_correct); + pixman_color_t bg = color_hex_to_pixman(term->colors.bg, gamma_correct); + + pixman_color_t cursor_color, text_color; + cursor_colors_for_cell( + term, start_cell, &fg, &bg, &cursor_color, &text_color, gamma_correct); + + int x = term->margins.left + (col_idx + start) * term->cell_width; + int y = term->margins.top + row_idx * term->cell_height; + + if (end == start) { + /* Bar */ + if (start >= 0) { + struct fcft_font *font = attrs_to_font(term, &start_cell->attrs); + draw_beam_cursor(term, buf->pix[0], font, &cursor_color, x, y); + } + term_ime_set_cursor_rect(term, x, y, 1, term->cell_height); + } + + else if (end > start) { + /* Hollow cursor */ + if (start >= 0 && end <= term->cols) { + int cols = end - start; + draw_hollow_block(term, buf->pix[0], &cursor_color, x, y, cols); + } + + term_ime_set_cursor_rect( + term, x, y, (end - start) * term->cell_width, term->cell_height); + } + } + + /* Restore original content (but do not render) */ + for (int i = 0; i < cells_used; i++) + row->cells[col_idx + i] = real_cells[i]; + free(real_cells); + + const int damage_x = term->margins.left + col_idx * term->cell_width; + const int damage_y = term->margins.top + row_idx * term->cell_height; + const int damage_w = cells_used * term->cell_width; + const int damage_h = term->cell_height; + + wl_surface_damage_buffer( + term->window->surface.surf, + damage_x, damage_y, damage_w, damage_h); +} +#endif + +static void +render_ime_preedit(struct terminal *term, struct buffer *buf) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) + render_ime_preedit_for_seat(term, &it->item, buf); + } +#endif +} + +static void +render_overlay_single_pixel(struct terminal *term, enum overlay_style style, + pixman_color_t color) +{ + struct wayland *wayl = term->wl; + struct wayl_sub_surface *overlay = &term->window->overlay; + struct wl_buffer *buf = NULL; + + /* + * In an ideal world, we'd only update the surface (i.e. commit + * any changes) if anything has actually changed. + * + * For technical reasons, we can't do that, since we can't + * determine whether the last committed buffer is still valid + * (i.e. does it correspond to the current overlay style, *and* + * does last frame's size match the current size?) + * + * What we _can_ do is use the fact that single-pixel buffers + * don't have a size; you have to use a viewport to "size" them. + * + * This means we can check if the last frame's overlay style is + * the same as the current size. If so, then we *know* that the + * currently attached buffer is valid, and we *don't* have to + * create a new single-pixel buffer. + * + * What we do *not* know if the *size* is still valid. This means + * we do have to do the viewport calls, and a surface commit. + * + * This is still better than *always* creating a new buffer. + */ + + assert(style == OVERLAY_UNICODE_MODE || style == OVERLAY_FLASH); + assert(wayl->single_pixel_manager != NULL); + assert(overlay->surface.viewport != NULL); + + quirk_weston_subsurface_desync_on(overlay->sub); + + if (style != term->render.last_overlay_style) { + buf = wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer( + wayl->single_pixel_manager, + (double)color.red / 0xffff * 0xffffffff, + (double)color.green / 0xffff * 0xffffffff, + (double)color.blue / 0xffff * 0xffffffff, + (double)color.alpha / 0xffff * 0xffffffff); + + wl_surface_set_buffer_scale(overlay->surface.surf, 1); + wl_surface_attach(overlay->surface.surf, buf, 0, 0); + } + + wp_viewport_set_destination( + overlay->surface.viewport, + roundf(term->width / term->scale), + roundf(term->height / term->scale)); + + wl_subsurface_set_position(overlay->sub, 0, 0); + + wl_surface_damage_buffer( + overlay->surface.surf, 0, 0, term->width, term->height); + + wl_surface_commit(overlay->surface.surf); + quirk_weston_subsurface_desync_off(overlay->sub); + + term->render.last_overlay_style = style; + + if (buf != NULL) { + wl_buffer_destroy(buf); + } +} + +void +render_overlay(struct terminal *term) +{ + struct wayl_sub_surface *overlay = &term->window->overlay; + const bool unicode_mode_active = term->unicode_mode.active; + + const enum overlay_style style = + term->flash.active ? OVERLAY_FLASH : + unicode_mode_active ? OVERLAY_UNICODE_MODE : + OVERLAY_NONE; + + if (likely(style == OVERLAY_NONE)) { + if (term->render.last_overlay_style != OVERLAY_NONE) { + /* Unmap overlay sub-surface */ + wl_surface_attach(overlay->surface.surf, NULL, 0, 0); + wl_surface_commit(overlay->surface.surf); + term->render.last_overlay_style = OVERLAY_NONE; + term->render.last_overlay_buf = NULL; + } + return; + } + + pixman_color_t color; + + switch (style) { + case OVERLAY_SEARCH: + case OVERLAY_UNICODE_MODE: + color = (pixman_color_t){0, 0, 0, 0x7fff}; + break; + + case OVERLAY_FLASH: + color = color_hex_to_pixman_with_alpha( + term->conf->colors_dark.flash, + term->conf->colors_dark.flash_alpha, + wayl_do_linear_blending(term->wl, term->conf)); + break; + + case OVERLAY_NONE: + xassert(false); + break; + } + + const bool single_pixel = + (style == OVERLAY_UNICODE_MODE || style == OVERLAY_FLASH) && + term->wl->single_pixel_manager != NULL && + overlay->surface.viewport != NULL; + + if (single_pixel) { + render_overlay_single_pixel(term, style, color); + return; + } + + struct buffer *buf = shm_get_buffer( + term->render.chains.overlay, term->width, term->height); + pixman_image_set_clip_region32(buf->pix[0], NULL); + + /* Bounding rectangle of damaged areas - for wl_surface_damage_buffer() */ + pixman_box32_t damage_bounds; + + if (style == OVERLAY_SEARCH) { + /* + * When possible, we only update the areas that have *changed* + * since the last frame. That means: + * + * - clearing/erasing cells that are now selected, but weren't + * in the last frame + * - dimming cells that were selected, but aren't anymore + * + * To do this, we save the last frame's selected cells as a + * pixman region. + * + * Then, we calculate the corresponding region for this + * frame's selected cells. + * + * Last frame's region minus this frame's region gives us the + * region that needs to be *dimmed* in this frame + * + * This frame's region minus last frame's region gives us the + * region that needs to be *cleared* in this frame. + * + * Finally, the union of the two "diff" regions above, gives + * us the total region affected by a change, in either way. We + * use this as the bounding box for the + * wl_surface_damage_buffer() call. + */ + pixman_region32_t *see_through = &term->render.last_overlay_clip; + pixman_region32_t old_see_through; + const bool buffer_reuse = + buf == term->render.last_overlay_buf && + style == term->render.last_overlay_style && + buf->age == 0; + + if (!buffer_reuse) { + /* Can't reuse last frame's damage - set to full window, + * to ensure *everything* is updated */ + pixman_region32_init_rect( + &old_see_through, 0, 0, buf->width, buf->height); + } else { + /* Use last frame's saved region */ + pixman_region32_init(&old_see_through); + pixman_region32_copy(&old_see_through, see_through); + } + + pixman_region32_clear(see_through); + + /* Build region consisting of all current search matches */ + struct search_match_iterator iter = search_matches_new_iter(term); + for (struct range match = search_matches_next(&iter); + match.start.row >= 0; + match = search_matches_next(&iter)) + { + int r = match.start.row; + int start_col = match.start.col; + const int end_row = match.end.row; + + while (true) { + const int end_col = + r == end_row ? match.end.col : term->cols - 1; + + int x = term->margins.left + start_col * term->cell_width; + int y = term->margins.top + r * term->cell_height; + int width = (end_col + 1 - start_col) * term->cell_width; + int height = 1 * term->cell_height; + + pixman_region32_union_rect( + see_through, see_through, x, y, width, height); + + if (++r > end_row) + break; + + start_col = 0; + } + } + + /* Areas that need to be cleared: cells that were dimmed in + * the last frame but is now see-through */ + pixman_region32_t new_see_through; + pixman_region32_init(&new_see_through); + + if (buffer_reuse) + pixman_region32_subtract(&new_see_through, see_through, &old_see_through); + else { + /* Buffer content is unknown - explicitly clear *all* + * current see-through areas */ + pixman_region32_copy(&new_see_through, see_through); + } + pixman_image_set_clip_region32(buf->pix[0], &new_see_through); + + /* Areas that need to be dimmed: cells that were cleared in + * the last frame but is not anymore */ + pixman_region32_t new_dimmed; + pixman_region32_init(&new_dimmed); + pixman_region32_subtract(&new_dimmed, &old_see_through, see_through); + pixman_region32_fini(&old_see_through); + + /* Total affected area */ + pixman_region32_t damage; + pixman_region32_init(&damage); + pixman_region32_union(&damage, &new_see_through, &new_dimmed); + damage_bounds = damage.extents; + + /* Clear cells that became selected in this frame. */ + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &(pixman_color_t){0}, 1, + &(pixman_rectangle16_t){0, 0, term->width, term->height}); + + /* Set clip region for the newly dimmed cells. The actual + * paint call is done below */ + pixman_image_set_clip_region32(buf->pix[0], &new_dimmed); + + pixman_region32_fini(&new_see_through); + pixman_region32_fini(&new_dimmed); + pixman_region32_fini(&damage); + } + + else if (buf == term->render.last_overlay_buf && + style == term->render.last_overlay_style) + { + xassert(style == OVERLAY_FLASH || style == OVERLAY_UNICODE_MODE); + shm_did_not_use_buf(buf); + return; + } else { + pixman_image_set_clip_region32(buf->pix[0], NULL); + damage_bounds = (pixman_box32_t){0, 0, buf->width, buf->height}; + } + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 1, + &(pixman_rectangle16_t){0, 0, term->width, term->height}); + + quirk_weston_subsurface_desync_on(overlay->sub); + wayl_surface_scale( + term->window, &overlay->surface, buf, term->scale); + wl_subsurface_set_position(overlay->sub, 0, 0); + wl_surface_attach(overlay->surface.surf, buf->wl_buf, 0, 0); + + wl_surface_damage_buffer( + overlay->surface.surf, + damage_bounds.x1, damage_bounds.y1, + damage_bounds.x2 - damage_bounds.x1, + damage_bounds.y2 - damage_bounds.y1); + + wl_surface_commit(overlay->surface.surf); + quirk_weston_subsurface_desync_off(overlay->sub); + + buf->age = 0; + term->render.last_overlay_buf = buf; + term->render.last_overlay_style = style; +} + +int +render_worker_thread(void *_ctx) +{ + struct render_worker_context *ctx = _ctx; + struct terminal *term = ctx->term; + const int my_id = ctx->my_id; + free(ctx); + + sigset_t mask; + sigfillset(&mask); + pthread_sigmask(SIG_SETMASK, &mask, NULL); + + char proc_title[16]; + snprintf(proc_title, sizeof(proc_title), "foot:render:%d", my_id); + + if (pthread_setname_np(pthread_self(), proc_title) < 0) + LOG_ERRNO("render worker %d: failed to set process title", my_id); + + sem_t *start = &term->render.workers.start; + sem_t *done = &term->render.workers.done; + mtx_t *lock = &term->render.workers.lock; + + while (true) { + sem_wait(start); + + struct buffer *buf = term->render.workers.buf; + + bool frame_done = false; + + /* Translate offset-relative cursor row to view-relative */ + struct coord cursor = {-1, -1}; + if (!term->hide_cursor) { + cursor = term->grid->cursor.point; + cursor.row += term->grid->offset; + cursor.row -= term->grid->view; + cursor.row &= term->grid->num_rows - 1; + } + + while (!frame_done) { + mtx_lock(lock); + xassert(tll_length(term->render.workers.queue) > 0); + + int row_no = tll_pop_front(term->render.workers.queue); + mtx_unlock(lock); + + switch (row_no) { + default: { + struct row *row = grid_row_in_view(term->grid, row_no); + int cursor_col = cursor.row == row_no ? cursor.col : -1; + + render_row(term, buf->pix[my_id], &buf->dirty[my_id], + row, row_no, cursor_col); + break; + } + + case -1: + frame_done = true; + sem_post(done); + break; + + case -2: + return 0; + + case -3: { + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) + clock_gettime(CLOCK_MONOTONIC, &term->render.workers.preapplied_damage.start); + + mtx_lock(&term->render.workers.preapplied_damage.lock); + buf = term->render.workers.preapplied_damage.buf; + xassert(buf != NULL); + + if (likely(term->render.last_buf != NULL)) { + mtx_unlock(&term->render.workers.preapplied_damage.lock); + + pixman_region32_t dmg; + pixman_region32_init(&dmg); + + if (buf->age == 0) + ; /* No need to do anything */ + else if (buf->age == 1) + pixman_region32_copy(&dmg, + &term->render.last_buf->dirty[0]); + else + pixman_region32_init_rect(&dmg, 0, 0, buf->width, + buf->height); + + pixman_image_set_clip_region32(buf->pix[my_id], &dmg); + pixman_image_composite32(PIXMAN_OP_SRC, + term->render.last_buf->pix[my_id], + NULL, buf->pix[my_id], 0, 0, 0, 0, 0, + 0, buf->width, buf->height); + + pixman_region32_fini(&dmg); + + buf->age = 0; + shm_unref(term->render.last_buf); + shm_addref(buf); + term->render.last_buf = buf; + + mtx_lock(&term->render.workers.preapplied_damage.lock); + } + + term->render.workers.preapplied_damage.buf = NULL; + cnd_signal(&term->render.workers.preapplied_damage.cond); + mtx_unlock(&term->render.workers.preapplied_damage.lock); + + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) + clock_gettime(CLOCK_MONOTONIC, &term->render.workers.preapplied_damage.stop); + + frame_done = true; + break; + } + } + } + }; + + return -1; +} + +void +render_wait_for_preapply_damage(struct terminal *term) +{ + if (!term->render.preapply_last_frame_damage) + return; + if (term->render.workers.preapplied_damage.buf == NULL) + return; + + mtx_lock(&term->render.workers.preapplied_damage.lock); + while (term->render.workers.preapplied_damage.buf != NULL) { + cnd_wait(&term->render.workers.preapplied_damage.cond, + &term->render.workers.preapplied_damage.lock); + } + mtx_unlock(&term->render.workers.preapplied_damage.lock); +} + +struct csd_data +get_csd_data(const struct terminal *term, enum csd_surface surf_idx) +{ + xassert(term->window->csd_mode == CSD_YES); + + const bool borders_visible = wayl_win_csd_borders_visible(term->window); + const bool title_visible = wayl_win_csd_titlebar_visible(term->window); + + const float scale = term->scale; + + const int border_width = borders_visible + ? roundf(term->conf->csd.border_width * scale) : 0; + + const int title_height = title_visible + ? roundf(term->conf->csd.title_height * scale) : 0; + + const int button_width = title_visible + ? roundf(term->conf->csd.button_width * scale) : 0; + + int remaining_width = term->width; + + const int button_close_width = remaining_width >= button_width ? button_width : 0; + remaining_width -= button_close_width; + const int button_close_start = remaining_width; + + const int button_maximize_width = remaining_width >= button_width && + term->window->wm_capabilities.maximize ? button_width : 0; + remaining_width -= button_maximize_width; + const int button_maximize_start = remaining_width; + + const int button_minimize_width = remaining_width >= button_width && + term->window->wm_capabilities.minimize ? button_width : 0; + remaining_width -= button_minimize_width; + const int button_minimize_start = remaining_width; + + /* + * With fractional scaling, we must ensure the offset, when + * divided by the scale (in set_position()), and the scaled back + * (by the compositor), matches the actual pixel count made up by + * the titlebar and the border. + */ + const int top_offset = roundf( + scale * (roundf(-title_height / scale) - roundf(border_width / scale))); + + const int top_bottom_width = roundf( + scale * (roundf(term->width / scale) + 2 * roundf(border_width / scale))); + + const int left_right_height = roundf( + scale * (roundf(title_height / scale) + roundf(term->height / scale))); + + switch (surf_idx) { + case CSD_SURF_TITLE: return (struct csd_data){ 0, -title_height, term->width, title_height}; + case CSD_SURF_LEFT: return (struct csd_data){-border_width, -title_height, border_width, left_right_height}; + case CSD_SURF_RIGHT: return (struct csd_data){ term->width, -title_height, border_width, left_right_height}; + case CSD_SURF_TOP: return (struct csd_data){-border_width, top_offset, top_bottom_width, border_width}; + case CSD_SURF_BOTTOM: return (struct csd_data){-border_width, term->height, top_bottom_width, border_width}; + + /* Positioned relative to CSD_SURF_TITLE */ + case CSD_SURF_MINIMIZE: return (struct csd_data){button_minimize_start, 0, button_minimize_width, title_height}; + case CSD_SURF_MAXIMIZE: return (struct csd_data){button_maximize_start, 0, button_maximize_width, title_height}; + case CSD_SURF_CLOSE: return (struct csd_data){ button_close_start, 0, button_close_width, title_height}; + + case CSD_SURF_COUNT: + break; + } + + BUG("Invalid csd_surface type"); + return (struct csd_data){0}; +} + +static void +csd_commit(struct terminal *term, struct wayl_surface *surf, struct buffer *buf) +{ + wayl_surface_scale(term->window, surf, buf, term->scale); + wl_surface_attach(surf->surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(surf->surf, 0, 0, buf->width, buf->height); + wl_surface_commit(surf->surf); +} + +static void +render_csd_part(struct terminal *term, + struct wl_surface *surf, struct buffer *buf, + int width, int height, pixman_color_t *color) +{ + xassert(term->window->csd_mode == CSD_YES); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], color, 1, + &(pixman_rectangle16_t){0, 0, buf->width, buf->height}); +} + +static void +render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, + struct fcft_font *font, struct buffer *buf, + const char32_t *text, uint32_t _fg, uint32_t _bg, + unsigned x) +{ + pixman_region32_t clip; + pixman_region32_init_rect(&clip, 0, 0, buf->width, buf->height); + pixman_image_set_clip_region32(buf->pix[0], &clip); + pixman_region32_fini(&clip); + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + uint16_t alpha = _bg >> 24 | (_bg >> 24 << 8); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &bg, 1, + &(pixman_rectangle16_t){0, 0, buf->width, buf->height}); + + pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); + const int x_ofs = term->font_x_ofs; + + const size_t len = c32len(text); + struct fcft_text_run *text_run = NULL; + const struct fcft_glyph **glyphs = NULL; + const struct fcft_glyph *_glyphs[len]; + size_t glyph_count = 0; + + if (fcft_capabilities() & FCFT_CAPABILITY_TEXT_RUN_SHAPING) { + text_run = fcft_rasterize_text_run_utf32( + font, len, (const char32_t *)text, term->font_subpixel); + + if (text_run != NULL) { + glyphs = text_run->glyphs; + glyph_count = text_run->count; + } + } + + if (glyphs == NULL) { + for (size_t i = 0; i < len; i++) { + const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( + font, text[i], term->font_subpixel); + + if (glyph == NULL) + continue; + + _glyphs[glyph_count++] = glyph; + } + + glyphs = _glyphs; + } + + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + + /* Calculate baseline */ + unsigned y; + { + const int line_height = buf->height; + const int font_height = max(font->height, font->ascent + font->descent); + const int glyph_top_y = round((line_height - font_height) / 2.); + y = term->font_y_ofs + glyph_top_y + font->ascent; + } + + for (size_t i = 0; i < glyph_count; i++) { + const struct fcft_glyph *glyph = glyphs[i]; + + if (unlikely(glyph->is_color_glyph)) { + pixman_image_composite32( + PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, + x + x_ofs + glyph->x, y - glyph->y, + glyph->width, glyph->height); + } else { + pixman_image_composite32( + PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, + x + x_ofs + glyph->x, y - glyph->y, + glyph->width, glyph->height); + } + + x += glyph->advance.x; + } + + fcft_text_run_destroy(text_run); + pixman_image_unref(src); + pixman_image_set_clip_region32(buf->pix[0], NULL); + + quirk_weston_subsurface_desync_on(sub_surf->sub); + wayl_surface_scale(term->window, &sub_surf->surface, buf, term->scale); + wl_surface_attach(sub_surf->surface.surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(sub_surf->surface.surf, 0, 0, buf->width, buf->height); + + if (alpha == 0xffff) { + struct wl_region *region = wl_compositor_create_region(term->wl->compositor); + if (region != NULL) { + wl_region_add(region, 0, 0, buf->width, buf->height); + wl_surface_set_opaque_region(sub_surf->surface.surf, region); + wl_region_destroy(region); + } + } else + wl_surface_set_opaque_region(sub_surf->surface.surf, NULL); + + wl_surface_commit(sub_surf->surface.surf); + quirk_weston_subsurface_desync_off(sub_surf->sub); +} + +static void +render_csd_title(struct terminal *term, const struct csd_data *info, + struct buffer *buf) +{ + xassert(term->window->csd_mode == CSD_YES); + + struct wayl_sub_surface *surf = &term->window->csd.surface[CSD_SURF_TITLE]; + if (info->width == 0 || info->height == 0) + return; + + uint32_t bg = term->conf->csd.color.title_set + ? term->conf->csd.color.title + : 0xffu << 24 | term->conf->colors_dark.fg; + uint32_t fg = term->conf->csd.color.buttons_set + ? term->conf->csd.color.buttons + : term->conf->colors_dark.bg; + + if (!term->visual_focus) { + bg = color_dim(term, bg); + fg = color_dim(term, fg); + } + + char32_t *_title_text = ambstoc32(term->window_title); + const char32_t *title_text = _title_text != NULL ? _title_text : U""; + + struct wl_window *win = term->window; + + const struct fcft_glyph *M = fcft_rasterize_char_utf32( + win->csd.font, U'M', term->font_subpixel); + + const int margin = M != NULL ? M->advance.x : win->csd.font->max_advance.x; + + render_osd(term, surf, win->csd.font, buf, title_text, fg, bg, margin); + csd_commit(term, &surf->surface, buf); + free(_title_text); +} + +static void +render_csd_border(struct terminal *term, enum csd_surface surf_idx, + const struct csd_data *info, struct buffer *buf) +{ + xassert(term->window->csd_mode == CSD_YES); + xassert(surf_idx >= CSD_SURF_LEFT && surf_idx <= CSD_SURF_BOTTOM); + + struct wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; + + if (info->width == 0 || info->height == 0) + return; + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + + { + /* Fully transparent - no need to do a color space transform */ + pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0, gamma_correct); + render_csd_part(term, surf->surf, buf, info->width, info->height, &color); + } + + /* + * The "visible" border. + */ + + float scale = term->scale; + int bwidth = (int)roundf(term->conf->csd.border_width * scale); + int vwidth = (int)roundf(term->conf->csd.border_width_visible * scale); /* Visible size */ + + xassert(bwidth >= vwidth); + + if (vwidth > 0) { + + const struct config *conf = term->conf; + int x = 0, y = 0, w = 0, h = 0; + + + switch (surf_idx) { + case CSD_SURF_TOP: + case CSD_SURF_BOTTOM: + x = bwidth - vwidth; + y = surf_idx == CSD_SURF_TOP ? info->height - vwidth : 0; + w = info->width - 2 * x; + h = vwidth; + break; + + case CSD_SURF_LEFT: + case CSD_SURF_RIGHT: + x = surf_idx == CSD_SURF_LEFT ? bwidth - vwidth : 0; + y = 0; + w = vwidth; + h = info->height; + break; + + case CSD_SURF_TITLE: + case CSD_SURF_MINIMIZE: + case CSD_SURF_MAXIMIZE: + case CSD_SURF_CLOSE: + case CSD_SURF_COUNT: + BUG("unexpected CSD surface type"); + } + + xassert(x >= 0); + xassert(y >= 0); + xassert(w >= 0); + xassert(h >= 0); + + xassert(x + w <= info->width); + xassert(y + h <= info->height); + + uint32_t _color = + conf->csd.color.border_set ? conf->csd.color.border : + conf->csd.color.title_set ? conf->csd.color.title : + 0xffu << 24 | term->conf->colors_dark.fg; + if (!term->visual_focus) + _color = color_dim(term, _color); + + uint16_t alpha = _color >> 24 | (_color >> 24 << 8); + pixman_color_t color = + color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 1, + &(pixman_rectangle16_t){x, y, w, h}); + } + + csd_commit(term, surf, buf); +} + +static pixman_color_t +get_csd_button_fg_color(const struct terminal *term) +{ + const struct config *conf = term->conf; + uint32_t _color = conf->colors_dark.bg; + uint16_t alpha = 0xffff; + + if (conf->csd.color.buttons_set) { + _color = conf->csd.color.buttons; + alpha = _color >> 24 | (_color >> 24 << 8); + } + + return color_hex_to_pixman_with_alpha( + _color, alpha, wayl_do_linear_blending(term->wl, term->conf)); +} + +static void +render_csd_button_minimize(struct terminal *term, struct buffer *buf) +{ + pixman_color_t color = get_csd_button_fg_color(term); + pixman_image_t *src = pixman_image_create_solid_fill(&color); + + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; + + int width = min(max_height, max_width); + int thick = min(width / 2, 1 * term->scale); + + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; + + xassert(x_margin + width - thick >= 0); + xassert(width - 2 * thick >= 0); + xassert(y_margin + width - thick >= 0); + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 1, + (pixman_rectangle16_t[]) { + {x_margin, y_margin + width - thick, width, thick} + }); + + pixman_image_unref(src); +} + +static void +render_csd_button_maximize_maximized( + struct terminal *term, struct buffer *buf) +{ + pixman_color_t color = get_csd_button_fg_color(term); + pixman_image_t *src = pixman_image_create_solid_fill(&color); + + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; + + int width = min(max_height, max_width); + int thick = min(width / 2, 1 * term->scale); + + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; + const int shrink = 1; + xassert(x_margin + width - thick >= 0); + xassert(width - 2 * thick >= 0); + xassert(y_margin + width - thick >= 0); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 4, + (pixman_rectangle16_t[]){ + {x_margin + shrink, y_margin + shrink, width - 2 * shrink, thick}, + { x_margin + shrink, y_margin + thick, thick, width - 2 * thick - shrink }, + { x_margin + width - thick - shrink, y_margin + thick, thick, width - 2 * thick - shrink }, + { x_margin + shrink, y_margin + width - thick - shrink, width - 2 * shrink, thick }}); + + pixman_image_unref(src); + +} + +static void +render_csd_button_maximize_window( + struct terminal *term, struct buffer *buf) +{ + pixman_color_t color = get_csd_button_fg_color(term); + pixman_image_t *src = pixman_image_create_solid_fill(&color); + + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; + + int width = min(max_height, max_width); + int thick = min(width / 2, 1 * term->scale); + + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; + + xassert(x_margin + width - thick >= 0); + xassert(width - 2 * thick >= 0); + xassert(y_margin + width - thick >= 0); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, 4, + (pixman_rectangle16_t[]) { + {x_margin, y_margin, width, thick}, + { x_margin, y_margin + thick, thick, width - 2 * thick }, + { x_margin + width - thick, y_margin + thick, thick, width - 2 * thick }, + { x_margin, y_margin + width - thick, width, thick } + }); + + pixman_image_unref(src); +} + +static void +render_csd_button_maximize(struct terminal *term, struct buffer *buf) +{ + if (term->window->is_maximized) + render_csd_button_maximize_maximized(term, buf); + else + render_csd_button_maximize_window(term, buf); +} + +static void +render_csd_button_close(struct terminal *term, struct buffer *buf) +{ + pixman_color_t color = get_csd_button_fg_color(term); + pixman_image_t *src = pixman_image_create_solid_fill(&color); + + const int max_height = buf->height / 3; + const int max_width = buf->width / 3; + + int width = min(max_height, max_width); + int thick = min(width / 2, 1 * term->scale); + const int x_margin = (buf->width - width) / 2; + const int y_margin = (buf->height - width) / 2; + + xassert(x_margin + width - thick >= 0); + xassert(width - 2 * thick >= 0); + xassert(y_margin + width - thick >= 0); + + pixman_triangle_t tri[4] = { + { + .p1 = { + .x = pixman_int_to_fixed(x_margin), + .y = pixman_int_to_fixed(y_margin + thick), + }, + .p2 = { + .x = pixman_int_to_fixed(x_margin + width - thick), + .y = pixman_int_to_fixed(y_margin + width), + }, + .p3 = { + .x = pixman_int_to_fixed(x_margin + thick), + .y = pixman_int_to_fixed(y_margin), + }, + }, + + { + .p1 = { + .x = pixman_int_to_fixed(x_margin + width), + .y = pixman_int_to_fixed(y_margin + width - thick), + }, + .p2 = { + .x = pixman_int_to_fixed(x_margin + thick), + .y = pixman_int_to_fixed(y_margin), + }, + .p3 = { + .x = pixman_int_to_fixed(x_margin + width - thick), + .y = pixman_int_to_fixed(y_margin + width), + }, + }, + + { + .p1 = { + .x = pixman_int_to_fixed(x_margin), + .y = pixman_int_to_fixed(y_margin + width - thick), + }, + .p2 = { + .x = pixman_int_to_fixed(x_margin + width), + .y = pixman_int_to_fixed(y_margin + thick), + }, + .p3 = { + .x = pixman_int_to_fixed(x_margin + thick), + .y = pixman_int_to_fixed(y_margin + width), + }, + }, + + { + .p1 = { + .x = pixman_int_to_fixed(x_margin + width), + .y = pixman_int_to_fixed(y_margin + thick), + }, + .p2 = { + .x = pixman_int_to_fixed(x_margin), + .y = pixman_int_to_fixed(y_margin + width - thick), + }, + .p3 = { + .x = pixman_int_to_fixed(x_margin + width - thick), + .y = pixman_int_to_fixed(y_margin), + }, + }, + }; + + pixman_composite_triangles( + PIXMAN_OP_OVER, src, buf->pix[0], PIXMAN_a1, + 0, 0, 0, 0, 4, tri); + + pixman_image_unref(src); +} + +static bool +any_pointer_is_on_button(const struct terminal *term, enum csd_surface csd_surface) +{ + if (unlikely(tll_length(term->wl->seats) == 0)) + return false; + + tll_foreach(term->wl->seats, it) { + const struct seat *seat = &it->item; + + if (seat->mouse.x < 0) + continue; + if (seat->mouse.y < 0) + continue; + + struct csd_data info = get_csd_data(term, csd_surface); + if (seat->mouse.x > info.width) + continue; + + if (seat->mouse.y > info.height) + continue; + return true; + } + + return false; +} + +static void +render_csd_button(struct terminal *term, enum csd_surface surf_idx, + const struct csd_data *info, struct buffer *buf) +{ + xassert(term->window->csd_mode == CSD_YES); + xassert(surf_idx >= CSD_SURF_MINIMIZE && surf_idx <= CSD_SURF_CLOSE); + + struct wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; + + if (info->width == 0 || info->height == 0) + return; + + uint32_t _color; + uint16_t alpha = 0xffff; + bool is_active = false; + bool is_set = false; + const uint32_t *conf_color = NULL; + + switch (surf_idx) { + case CSD_SURF_MINIMIZE: + _color = term->conf->colors_dark.table[4]; /* blue */ + is_set = term->conf->csd.color.minimize_set; + conf_color = &term->conf->csd.color.minimize; + is_active = term->active_surface == TERM_SURF_BUTTON_MINIMIZE && + any_pointer_is_on_button(term, CSD_SURF_MINIMIZE); + break; + + case CSD_SURF_MAXIMIZE: + _color = term->conf->colors_dark.table[2]; /* green */ + is_set = term->conf->csd.color.maximize_set; + conf_color = &term->conf->csd.color.maximize; + is_active = term->active_surface == TERM_SURF_BUTTON_MAXIMIZE && + any_pointer_is_on_button(term, CSD_SURF_MAXIMIZE); + break; + + case CSD_SURF_CLOSE: + _color = term->conf->colors_dark.table[1]; /* red */ + is_set = term->conf->csd.color.close_set; + conf_color = &term->conf->csd.color.quit; + is_active = term->active_surface == TERM_SURF_BUTTON_CLOSE && + any_pointer_is_on_button(term, CSD_SURF_CLOSE); + break; + + default: + BUG("unhandled surface type: %u", (unsigned)surf_idx); + break; + } + + if (is_active) { + if (is_set) { + _color = *conf_color; + alpha = _color >> 24 | (_color >> 24 << 8); + } + } else { + _color = 0; + alpha = 0; + } + + if (!term->visual_focus) + _color = color_dim(term, _color); + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); + render_csd_part(term, surf->surf, buf, info->width, info->height, &color); + + switch (surf_idx) { + case CSD_SURF_MINIMIZE: render_csd_button_minimize(term, buf); break; + case CSD_SURF_MAXIMIZE: render_csd_button_maximize(term, buf); break; + case CSD_SURF_CLOSE: render_csd_button_close(term, buf); break; + + default: + BUG("unhandled surface type: %u", (unsigned)surf_idx); + break; + } + + csd_commit(term, surf, buf); +} + +static void +render_csd(struct terminal *term) +{ + xassert(term->window->csd_mode == CSD_YES); + + if (term->window->is_fullscreen) + return; + + const float scale = term->scale; + struct csd_data infos[CSD_SURF_COUNT]; + int widths[CSD_SURF_COUNT]; + int heights[CSD_SURF_COUNT]; + + for (size_t i = 0; i < CSD_SURF_COUNT; i++) { + infos[i] = get_csd_data(term, i); + const int x = infos[i].x; + const int y = infos[i].y; + const int width = infos[i].width; + const int height = infos[i].height; + + struct wl_surface *surf = term->window->csd.surface[i].surface.surf; + struct wl_subsurface *sub = term->window->csd.surface[i].sub; + + xassert(surf != NULL); + xassert(sub != NULL); + + if (width == 0 || height == 0) { + widths[i] = heights[i] = 0; + wl_subsurface_set_position(sub, 0, 0); + wl_surface_attach(surf, NULL, 0, 0); + wl_surface_commit(surf); + continue; + } + + widths[i] = width; + heights[i] = height; + wl_subsurface_set_position(sub, roundf(x / scale), roundf(y / scale)); + } + + struct buffer *bufs[CSD_SURF_COUNT]; + shm_get_many(term->render.chains.csd, CSD_SURF_COUNT, widths, heights, bufs); + + for (size_t i = CSD_SURF_LEFT; i <= CSD_SURF_BOTTOM; i++) + render_csd_border(term, i, &infos[i], bufs[i]); + for (size_t i = CSD_SURF_MINIMIZE; i <= CSD_SURF_CLOSE; i++) + render_csd_button(term, i, &infos[i], bufs[i]); + render_csd_title(term, &infos[CSD_SURF_TITLE], bufs[CSD_SURF_TITLE]); +} + +static void +render_scrollback_position(struct terminal *term) +{ + if (term->conf->scrollback.indicator.position == SCROLLBACK_INDICATOR_POSITION_NONE) + return; + + struct wl_window *win = term->window; + + if (term->grid->view == term->grid->offset) { + if (win->scrollback_indicator.surface.surf != NULL) + wayl_win_subsurface_destroy(&win->scrollback_indicator); + return; + } + + if (win->scrollback_indicator.surface.surf == NULL) { + if (!wayl_win_subsurface_new( + win, &win->scrollback_indicator, false)) + { + LOG_ERR("failed to create scrollback indicator surface"); + return; + } + } + + xassert(win->scrollback_indicator.surface.surf != NULL); + xassert(win->scrollback_indicator.sub != NULL); + + /* Find absolute row number of the scrollback start */ + int scrollback_start = term->grid->offset + term->rows; + int empty_rows = 0; + while (term->grid->rows[scrollback_start & (term->grid->num_rows - 1)] == NULL) { + scrollback_start++; + empty_rows++; + } + + /* Rebase viewport against scrollback start (so that 0 is at + * the beginning of the scrollback) */ + int rebased_view = term->grid->view - scrollback_start + term->grid->num_rows; + rebased_view &= term->grid->num_rows - 1; + + /* How much of the scrollback is actually used? */ + int populated_rows = term->grid->num_rows - empty_rows; + xassert(populated_rows > 0); + xassert(populated_rows <= term->grid->num_rows); + + /* + * How far down in the scrollback we are. + * + * 0% -> at the beginning of the scrollback + * 100% -> at the bottom, i.e. where new lines are inserted + */ + double percent = + rebased_view + term->rows == populated_rows + ? 1.0 + : (double)rebased_view / (populated_rows - term->rows); + + char32_t _text[64]; + const char32_t *text = _text; + int cell_count = 0; + + /* *What* to render */ + switch (term->conf->scrollback.indicator.format) { + case SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE: { + char percent_str[8]; + snprintf(percent_str, sizeof(percent_str), "%u%%", (int)(100 * percent)); + mbstoc32(_text, percent_str, ALEN(_text)); + cell_count = 3; + break; + } + + case SCROLLBACK_INDICATOR_FORMAT_LINENO: { + char lineno_str[64]; + snprintf(lineno_str, sizeof(lineno_str), "%d", rebased_view + 1); + mbstoc32(_text, lineno_str, ALEN(_text)); + cell_count = (int)ceilf(log10f(term->grid->num_rows)); + break; + } + + case SCROLLBACK_INDICATOR_FORMAT_TEXT: + text = term->conf->scrollback.indicator.text; + cell_count = c32len(text); + break; + } + + const float scale = term->scale; + const int margin = (int)roundf(3. * scale); + + int width = margin + cell_count * term->cell_width + margin; + int height = margin + term->cell_height + margin; + + width = roundf(scale * ceilf(width / scale)); + height = roundf(scale * ceilf(height / scale)); + + /* *Where* to render - parent relative coordinates */ + int surf_top = 0; + switch (term->conf->scrollback.indicator.position) { + case SCROLLBACK_INDICATOR_POSITION_NONE: + BUG("Invalid scrollback indicator position type"); + return; + + case SCROLLBACK_INDICATOR_POSITION_FIXED: + surf_top = term->cell_height - margin; + break; + + case SCROLLBACK_INDICATOR_POSITION_RELATIVE: { + int lines = term->rows - 2; /* Avoid using first and last rows */ + if (term->is_searching) { + /* Make sure we don't collide with the scrollback search box */ + lines--; + } + + lines = max(lines, 0); + + int pixels = max(lines * term->cell_height - height + 2 * margin, 0); + surf_top = term->cell_height - margin + (int)(percent * pixels); + break; + } + } + + int x = term->width - margin - width; + int y = term->margins.top + surf_top; + + x = roundf(scale * ceilf(x / scale)); + y = roundf(scale * ceilf(y / scale)); + + if (y + height > term->height) { + wl_surface_attach(win->scrollback_indicator.surface.surf, NULL, 0, 0); + wl_surface_commit(win->scrollback_indicator.surface.surf); + return; + } + + struct buffer_chain *chain = term->render.chains.scrollback_indicator; + struct buffer *buf = shm_get_buffer(chain, width, height); + + wl_subsurface_set_position( + win->scrollback_indicator.sub, roundf(x / scale), roundf(y / scale)); + + uint32_t fg = term->colors.table[0]; + uint32_t bg = term->colors.table[8 + 4]; + if (term->conf->colors_dark.use_custom.scrollback_indicator) { + fg = term->conf->colors_dark.scrollback_indicator.fg; + bg = term->conf->colors_dark.scrollback_indicator.bg; + } + + render_osd( + term, + &win->scrollback_indicator, + term->fonts[0], buf, text, + fg, 0xffu << 24 | bg, + width - margin - c32len(text) * term->cell_width); +} + +static void +render_render_timer(struct terminal *term, struct timespec render_time) +{ + struct wl_window *win = term->window; + + char usecs_str[256]; + double usecs = render_time.tv_sec * 1000000 + render_time.tv_nsec / 1000.0; + snprintf(usecs_str, sizeof(usecs_str), "%.2f µs", usecs); + + char32_t text[256]; + mbstoc32(text, usecs_str, ALEN(text)); + + const float scale = term->scale; + const int cell_count = c32len(text); + const int margin = (int)roundf(3. * scale); + + int width = margin + cell_count * term->cell_width + margin; + int height = margin + term->cell_height + margin; + + width = roundf(scale * ceilf(width / scale)); + height = roundf(scale * ceilf(height / scale)); + + struct buffer_chain *chain = term->render.chains.render_timer; + struct buffer *buf = shm_get_buffer(chain, width, height); + + wl_subsurface_set_position( + win->render_timer.sub, + roundf(margin / scale), + roundf((term->margins.top + term->cell_height - margin) / scale)); + + render_osd( + term, + &win->render_timer, + term->fonts[0], buf, text, + term->colors.table[0], 0xffu << 24 | term->colors.table[8 + 1], + margin); +} + +static void frame_callback( + void *data, struct wl_callback *wl_callback, uint32_t callback_data); + +static const struct wl_callback_listener frame_listener = { + .done = &frame_callback, +}; + +static void +force_full_repaint(struct terminal *term, struct buffer *buf) +{ + tll_free(term->grid->scroll_damage); + render_margin(term, buf, 0, term->rows, true); + term_damage_view(term); +} + +static void +reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old) +{ + if (new->age > 1) { + memcpy(new->data, old->data, new->height * new->stride); + return; + } + + pixman_region32_t dirty; + pixman_region32_init(&dirty); + + /* + * Figure out current frame's damage region + * + * If current frame doesn't have any scroll damage, we can simply + * subtract this frame's damage from the last frame's damage. That + * way, we don't have to copy areas from the old frame that'll + * just get overwritten by current frame. + * + * Note that this is row based. A "half damaged" row is not + * excluded. I.e. the entire row will be copied from the old frame + * to the new, and then when actually rendering the new frame, the + * updated cells will overwrite parts of the copied row. + * + * Since we're scanning the entire viewport anyway, we also track + * whether *all* cells are to be updated. In this case, just force + * a full re-rendering, and don't copy anything from the old + * frame. + */ + bool full_repaint_needed = true; + + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); + + if (!row->dirty) { + full_repaint_needed = false; + continue; + } + + bool row_all_dirty = true; + for (int c = 0; c < term->cols; c++) { + if (row->cells[c].attrs.clean) { + row_all_dirty = false; + full_repaint_needed = false; + break; + } + } + + if (row_all_dirty) { + pixman_region32_union_rect( + &dirty, &dirty, + term->margins.left, + term->margins.top + r * term->cell_height, + term->width - term->margins.left - term->margins.right, + term->cell_height); + } + } + + if (full_repaint_needed) { + force_full_repaint(term, new); + return; + } + + /* + * TODO: re-apply last frame's scroll damage + * + * We used to do this, but it turned out to be buggy. If we decide + * to re-add it, this is where to do it. Note that we'd also have + * to remove the updates to buf->dirty from grid_render_scroll() + * and grid_render_scroll_reverse(). + */ + + if (tll_length(term->grid->scroll_damage) == 0) { + /* + * We can only subtract current frame's damage from the old + * frame's if we don't have any scroll damage. + * + * If we do have scroll damage, the damage region we + * calculated above is not (yet) valid - we need to apply the + * current frame's scroll damage *first*. This is done later, + * when rendering the frame. + */ + pixman_region32_subtract(&dirty, &old->dirty[0], &dirty); + pixman_image_set_clip_region32(new->pix[0], &dirty); + } else { + /* Copy *all* of last frame's damaged areas */ + pixman_image_set_clip_region32(new->pix[0], &old->dirty[0]); + } + + pixman_image_composite32( + PIXMAN_OP_SRC, old->pix[0], NULL, new->pix[0], + 0, 0, 0, 0, 0, 0, term->width, term->height); + + pixman_image_set_clip_region32(new->pix[0], NULL); + pixman_region32_fini(&dirty); +} + +static void +dirty_old_cursor(struct terminal *term) +{ + if (term->render.last_cursor.row != NULL && !term->render.last_cursor.hidden) { + struct row *row = term->render.last_cursor.row; + struct cell *cell = &row->cells[term->render.last_cursor.col]; + cell->attrs.clean = 0; + row->dirty = true; + } + + /* Remember current cursor position, for the next frame */ + term->render.last_cursor.row = grid_row(term->grid, term->grid->cursor.point.row); + term->render.last_cursor.col = term->grid->cursor.point.col; + term->render.last_cursor.hidden = term->hide_cursor; +} + +static void +dirty_cursor(struct terminal *term) +{ + if (term->hide_cursor) + return; + + const struct coord *cursor = &term->grid->cursor.point; + + struct row *row = grid_row(term->grid, cursor->row); + struct cell *cell = &row->cells[cursor->col]; + cell->attrs.clean = 0; + row->dirty = true; +} + +static void +grid_render(struct terminal *term) +{ + if (term->shutdown.in_progress) + return; + + struct timespec start_time; + struct timespec start_wait_preapply = {0}, stop_wait_preapply = {0}; + struct timespec start_double_buffering = {0}, stop_double_buffering = {0}; + + /* Might be a thread doing pre-applied damage */ + if (unlikely(term->render.preapply_last_frame_damage && + term->render.workers.preapplied_damage.buf != NULL)) + { + clock_gettime(CLOCK_MONOTONIC, &start_wait_preapply); + render_wait_for_preapply_damage(term); + clock_gettime(CLOCK_MONOTONIC, &stop_wait_preapply); + } + + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) + clock_gettime(CLOCK_MONOTONIC, &start_time); + + xassert(term->width > 0); + xassert(term->height > 0); + + struct buffer_chain *chain = term->render.chains.grid; + struct buffer *buf = shm_get_buffer(chain, term->width, term->height); + + /* Dirty old and current cursor cell, to ensure they're repainted */ + dirty_old_cursor(term); + dirty_cursor(term); + + LOG_DBG("buffer age: %u (%p)", buf->age, (void *)buf); + + if (term->render.last_buf == NULL || + term->render.last_buf->width != buf->width || + term->render.last_buf->height != buf->height || + term->render.margins) + { + force_full_repaint(term, buf); + } + + else if (buf->age > 0) { + LOG_DBG("buffer age: %u (%p)", buf->age, (void *)buf); + + xassert(term->render.last_buf != NULL); + xassert(term->render.last_buf != buf); + xassert(term->render.last_buf->width == buf->width); + xassert(term->render.last_buf->height == buf->height); + + if (++term->render.frames_since_last_immediate_release > 10) { + static bool have_warned = false; + + if (!term->render.preapply_last_frame_damage && + term->conf->tweak.preapply_damage && + term->render.workers.count > 0) + { + LOG_INFO("enabling pre-applied frame damage"); + term->render.preapply_last_frame_damage = true; + } else if (!have_warned && !term->render.preapply_last_frame_damage) { + LOG_WARN("compositor is not releasing buffers immediately; " + "expect lower rendering performance"); + have_warned = true; + } + } + + clock_gettime(CLOCK_MONOTONIC, &start_double_buffering); + reapply_old_damage(term, buf, term->render.last_buf); + clock_gettime(CLOCK_MONOTONIC, &stop_double_buffering); + } else if (!term->render.preapply_last_frame_damage) { + term->render.frames_since_last_immediate_release = 0; + } + + if (term->render.last_buf != NULL) { + shm_unref(term->render.last_buf); + term->render.last_buf = NULL; + } + + term->render.last_buf = buf; + shm_addref(buf); + buf->age = 0; + + + tll_foreach(term->grid->scroll_damage, it) { + switch (it->item.type) { + case DAMAGE_SCROLL: + if (term->grid->view == term->grid->offset) + grid_render_scroll(term, buf, &it->item); + break; + + case DAMAGE_SCROLL_REVERSE: + if (term->grid->view == term->grid->offset) + grid_render_scroll_reverse(term, buf, &it->item); + break; + + case DAMAGE_SCROLL_IN_VIEW: + grid_render_scroll(term, buf, &it->item); + break; + + case DAMAGE_SCROLL_REVERSE_IN_VIEW: + grid_render_scroll_reverse(term, buf, &it->item); + break; + } + + tll_remove(term->grid->scroll_damage, it); + } + + /* + * Ensure selected cells have their 'selected' bit set. This is + * normally "automatically" true - the bit is set when the + * selection is made. + * + * However, if the cell is updated (printed to) while the + * selection is active, the 'selected' bit is cleared. Checking + * for this and re-setting the bit in term_print() is too + * expensive performance wise. + * + * Instead, we synchronize the selection bits here and now. This + * makes the performance impact linear to the number of selected + * cells rather than to the number of updated cells. + * + * (note that selection_dirty_cells() will not set the dirty flag + * on cells where the 'selected' bit is already set) + */ + selection_dirty_cells(term); + + /* Translate offset-relative row to view-relative, unless cursor + * is hidden, then we just set it to -1 */ + struct coord cursor = {-1, -1}; + if (!term->hide_cursor) { + cursor = term->grid->cursor.point; + cursor.row += term->grid->offset; + cursor.row -= term->grid->view; + cursor.row &= term->grid->num_rows - 1; + } + + if (term->conf->tweak.overflowing_glyphs) { + /* + * Pre-pass to dirty cells affected by overflowing glyphs. + * + * Given any two pair of cells where the first cell is + * overflowing into the second, *both* cells must be + * re-rendered if any one of them is dirty. + * + * Thus, given a string of overflowing glyphs, with a single + * dirty cell in the middle, we need to re-render the entire + * string. + */ + for (int r = 0; r < term->rows; r++) { + struct row *row = grid_row_in_view(term->grid, r); + + if (!row->dirty) + continue; + + /* Loop row from left to right, looking for dirty cells */ + for (struct cell *cell = &row->cells[0]; + cell < &row->cells[term->cols]; + cell++) + { + if (cell->attrs.clean) + continue; + + /* + * Cell is dirty, go back and dirty previous cells, if + * they are overflowing. + * + * As soon as we see a non-overflowing cell we can + * stop, since it isn't affecting the string of + * overflowing glyphs that follows it. + * + * As soon as we see a dirty cell, we can stop, since + * that means we've already handled it (remember the + * outer loop goes from left to right). + */ + for (struct cell *c = cell - 1; c >= &row->cells[0]; c--) { + if (c->attrs.confined) + break; + if (!c->attrs.clean) + break; + c->attrs.clean = false; + } + + /* + * Now move forward, dirtying all cells until we hit a + * non-overflowing cell. + * + * Note that the first non-overflowing cell must be + * re-rendered as well, but any cell *after* that is + * unaffected by the string of overflowing glyphs + * we're dealing with right now. + * + * For performance, this iterates the *outer* loop's + * cell pointer - no point in re-checking all these + * glyphs again, in the outer loop. + */ + for (; cell < &row->cells[term->cols]; cell++) { + cell->attrs.clean = false; + if (cell->attrs.confined) + break; + } + } + } + } + +#if defined(_DEBUG) + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); + + if (row->dirty) { + bool all_clean = true; + for (int c = 0; c < term->cols; c++) { + if (!row->cells[c].attrs.clean) { + all_clean = false; + break; + } + } + if (all_clean) + BUG("row #%d is dirty, but all cells are marked as clean", r); + } else { + for (int c = 0; c < term->cols; c++) { + if (!row->cells[c].attrs.clean) + BUG("row #%d is clean, but cell #%d is dirty", r, c); + } + } + } +#endif + + pixman_region32_t damage; + pixman_region32_init(&damage); + + render_sixel_images(term, buf->pix[0], &damage, &cursor); + + + if (term->render.workers.count > 0) { + mtx_lock(&term->render.workers.lock); + term->render.workers.buf = buf; + for (size_t i = 0; i < term->render.workers.count; i++) + sem_post(&term->render.workers.start); + + xassert(tll_length(term->render.workers.queue) == 0); + } + + for (int r = 0; r < term->rows; r++) { + struct row *row = grid_row_in_view(term->grid, r); + + if (!row->dirty) + continue; + + row->dirty = false; + + if (term->render.workers.count > 0) + tll_push_back(term->render.workers.queue, r); + + else { + /* TODO: damage region */ + int cursor_col = cursor.row == r ? cursor.col : -1; + render_row(term, buf->pix[0], &damage, row, r, cursor_col); + } + } + + /* Signal workers the frame is done */ + if (term->render.workers.count > 0) { + for (size_t i = 0; i < term->render.workers.count; i++) + tll_push_back(term->render.workers.queue, -1); + mtx_unlock(&term->render.workers.lock); + + for (size_t i = 0; i < term->render.workers.count; i++) + sem_wait(&term->render.workers.done); + term->render.workers.buf = NULL; + } + + for (size_t i = 0; i < term->render.workers.count; i++) + pixman_region32_union(&damage, &damage, &buf->dirty[i + 1]); + + pixman_region32_union(&buf->dirty[0], &buf->dirty[0], &damage); + + { + int box_count = 0; + pixman_box32_t *boxes = pixman_region32_rectangles(&damage, &box_count); + + for (size_t i = 0; i < box_count; i++) { + wl_surface_damage_buffer( + term->window->surface.surf, + boxes[i].x1, boxes[i].y1, + boxes[i].x2 - boxes[i].x1, boxes[i].y2 - boxes[i].y1); + } + } + + pixman_region32_fini(&damage); + + render_overlay(term); + render_ime_preedit(term, buf); + render_scrollback_position(term); + + if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) { + struct timespec end_time; + clock_gettime(CLOCK_MONOTONIC, &end_time); + + struct timespec wait_time; + timespec_sub(&stop_wait_preapply, &start_wait_preapply, &wait_time); + + struct timespec render_time; + timespec_sub(&end_time, &start_time, &render_time); + + struct timespec double_buffering_time; + timespec_sub(&stop_double_buffering, &start_double_buffering, &double_buffering_time); + + struct timespec preapply_damage; + timespec_sub(&term->render.workers.preapplied_damage.stop, + &term->render.workers.preapplied_damage.start, + &preapply_damage); + + struct timespec total_render_time; + timespec_add(&render_time, &double_buffering_time, &total_render_time); + timespec_add(&wait_time, &total_render_time, &total_render_time); + + switch (term->conf->tweak.render_timer) { + case RENDER_TIMER_LOG: + case RENDER_TIMER_BOTH: + LOG_INFO( + "frame rendered in %lds %9ldns " + "(%lds %9ldns wait, %lds %9ldns rendering, %lds %9ldns double buffering) not included: %lds %ldns pre-apply damage", + (long)total_render_time.tv_sec, + total_render_time.tv_nsec, + (long)wait_time.tv_sec, + wait_time.tv_nsec, + (long)render_time.tv_sec, + render_time.tv_nsec, + (long)double_buffering_time.tv_sec, + double_buffering_time.tv_nsec, + (long)preapply_damage.tv_sec, + preapply_damage.tv_nsec); + break; + + case RENDER_TIMER_OSD: + case RENDER_TIMER_NONE: + break; + } + + switch (term->conf->tweak.render_timer) { + case RENDER_TIMER_OSD: + case RENDER_TIMER_BOTH: + render_render_timer(term, total_render_time); + break; + + case RENDER_TIMER_LOG: + case RENDER_TIMER_NONE: + break; + } + } + + xassert(term->grid->offset >= 0 && term->grid->offset < term->grid->num_rows); + xassert(term->grid->view >= 0 && term->grid->view < term->grid->num_rows); + + xassert(term->window->frame_callback == NULL); + term->window->frame_callback = wl_surface_frame(term->window->surface.surf); + wl_callback_add_listener(term->window->frame_callback, &frame_listener, term); + + wayl_win_scale(term->window, buf); + + if (term->wl->presentation != NULL && term->conf->presentation_timings) { + struct timespec commit_time; + clock_gettime(term->wl->presentation_clock_id, &commit_time); + + struct wp_presentation_feedback *feedback = wp_presentation_feedback( + term->wl->presentation, term->window->surface.surf); + + if (feedback == NULL) { + LOG_WARN("failed to create presentation feedback"); + } else { + struct presentation_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct presentation_context){ + .term = term, + .input.tv_sec = term->render.input_time.tv_sec, + .input.tv_usec = term->render.input_time.tv_nsec / 1000, + .commit.tv_sec = commit_time.tv_sec, + .commit.tv_usec = commit_time.tv_nsec / 1000, + }; + + wp_presentation_feedback_add_listener( + feedback, &presentation_feedback_listener, ctx); + + term->render.input_time.tv_sec = 0; + term->render.input_time.tv_nsec = 0; + } + } + + if (term->conf->tweak.damage_whole_window) { + wl_surface_damage_buffer( + term->window->surface.surf, 0, 0, INT32_MAX, INT32_MAX); + } + + wl_surface_attach(term->window->surface.surf, buf->wl_buf, 0, 0); + wl_surface_commit(term->window->surface.surf); +} + +static void +render_search_box(struct terminal *term) +{ + xassert(term->window->search.sub != NULL); + + /* + * We treat the search box pretty much like a row of cells. That + * is, a glyph is either 1 or 2 (or more) "cells" wide. + * + * The search 'length', and 'cursor' (position) is in + * *characters*, not cells. This means we need to translate from + * character count to cell count when calculating the length of + * the search box, where in the search string we should start + * rendering etc. + */ + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + /* TODO: do we want to/need to handle multi-seat? */ + struct seat *ime_seat = NULL; + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) { + ime_seat = &it->item; + break; + } + } + + size_t text_len = term->search.len; + if (ime_seat != NULL && ime_seat->ime.preedit.text != NULL) + text_len += c32len(ime_seat->ime.preedit.text); + + char32_t *text = xmalloc((text_len + 1) * sizeof(char32_t)); + text[0] = U'\0'; + + /* Copy everything up to the cursor */ + c32ncpy(text, term->search.buf, term->search.cursor); + text[term->search.cursor] = U'\0'; + + /* Insert pre-edit text at cursor */ + if (ime_seat != NULL && ime_seat->ime.preedit.text != NULL) + c32cat(text, ime_seat->ime.preedit.text); + + /* And finally everything after the cursor */ + c32ncat(text, &term->search.buf[term->search.cursor], + term->search.len - term->search.cursor); +#else + const char32_t *text = term->search.buf; + const size_t text_len = term->search.len; +#endif + + /* Calculate the width of each character */ + int widths[text_len + 1]; + for (size_t i = 0; i < text_len; i++) + widths[i] = max(0, c32width(text[i])); + widths[text_len] = 0; + + const size_t total_cells = c32swidth(text, text_len); + const size_t wanted_visible_cells = max(20, total_cells); + + /* + * Build status string: " Aa Wb Re 3/17 ↻" + * Aa - case mode (blank=smart, A=sensitive, a=insensitive) + * Wb - whole-word toggle + * Re - regex toggle + * N/M - match counter + * ↻ - wrap indicator + */ + char32_t status[64]; + size_t st_len = 0; + + if (term->search.case_mode == SEARCH_CASE_SENSITIVE) { + status[st_len++] = U'A'; status[st_len++] = U'a'; + status[st_len++] = U' '; + } else if (term->search.case_mode == SEARCH_CASE_INSENSITIVE) { + status[st_len++] = U'a'; status[st_len++] = U'a'; + status[st_len++] = U' '; + } + if (term->search.whole_word) { + status[st_len++] = U'W'; status[st_len++] = U'b'; + status[st_len++] = U' '; + } + if (term->search.regex) { + status[st_len++] = U'.'; status[st_len++] = U'*'; + status[st_len++] = U' '; + } + if (term->search.wrapped) { + status[st_len++] = U'~'; status[st_len++] = U' '; + } + if (term->search.len > 0 && term->search.total_count > 0) { + char tmp[32]; + snprintf(tmp, sizeof(tmp), "%zu/%zu", + term->search.current_idx, term->search.total_count); + for (size_t i = 0; tmp[i] != '\0' && st_len < ALEN(status) - 1; i++) + status[st_len++] = (char32_t)tmp[i]; + status[st_len++] = U' '; + } + status[st_len] = U'\0'; + const size_t status_cells = c32swidth(status, st_len); + + const float scale = term->scale; + xassert(scale >= 1.); + const size_t margin = (size_t)roundf(3 * scale); + const size_t outer_margin = (size_t)roundf(6 * scale); + const size_t close_gap = (size_t)roundf(4 * scale); + const size_t close_w = term->cell_width; + const size_t status_w = status_cells * term->cell_width; + const size_t chrome_w = margin + status_w + close_gap + close_w + margin; + + const size_t max_box_w = term->width > (int)(2 * outer_margin) + ? (size_t)term->width - 2 * outer_margin : (size_t)term->width; + const size_t want_box_w = chrome_w + wanted_visible_cells * term->cell_width; + + size_t width = min(want_box_w, max_box_w); + size_t height = min( + term->height - 2 * outer_margin, + margin + term->cell_height + margin); + + width = roundf(scale * ceilf(width / scale)); + height = roundf(scale * ceilf(height / scale)); + + const size_t text_area_w = width > chrome_w ? width - chrome_w : 0; + const size_t visible_cells = text_area_w / term->cell_width; + size_t glyph_offset = term->render.search_glyph_offset; + + struct buffer_chain *chain = term->render.chains.search; + struct buffer *buf = shm_get_buffer(chain, width, height); + + pixman_region32_t clip; + pixman_region32_init_rect(&clip, 0, 0, width, height); + pixman_image_set_clip_region32(buf->pix[0], &clip); + pixman_region32_fini(&clip); + +#define WINDOW_X(x) ((int)term->width - (int)width - (int)outer_margin + (x)) +#define WINDOW_Y(y) ((int)outer_margin + (y)) + + /* Use the terminal's own bg/fg for the popup, tinted to indicate + * the search state. */ + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + + uint32_t bg_hex = term->colors.bg; + uint32_t fg_hex = term->colors.fg; + + if (term->search.len > 0 && term->search.match_len == 0) { + /* No match — red */ + bg_hex = term->colors.table[1]; + fg_hex = term->colors.table[0]; + } else if (term->search.wrapped) { + /* Wrapped — yellow */ + bg_hex = term->colors.table[3]; + fg_hex = term->colors.table[0]; + } + + const pixman_color_t color = color_hex_to_pixman(bg_hex, gamma_correct); + + pixman_image_fill_rectangles( + PIXMAN_OP_SRC, buf->pix[0], &color, + 1, &(pixman_rectangle16_t){0, 0, width, height}); + + struct fcft_font *font = term->fonts[0]; + const int x_left = margin; + const int x_ofs = term->font_x_ofs; + int x = x_left; + int y = margin; + + pixman_color_t fg = color_hex_to_pixman(fg_hex, gamma_correct); + + /* Move offset we start rendering at, to ensure the cursor is visible */ + for (size_t i = 0, cell_idx = 0; i <= term->search.cursor; cell_idx += widths[i], i++) { + if (i != term->search.cursor) + continue; + +#if (FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (ime_seat != NULL && ime_seat->ime.preedit.cells != NULL) { + if (ime_seat->ime.preedit.cursor.start == ime_seat->ime.preedit.cursor.end) { + /* All IME's I've seen so far keeps the cursor at + * index 0, so ensure the *end* of the pre-edit string + * is visible */ + cell_idx += ime_seat->ime.preedit.count; + } else { + /* Try to predict in which direction we'll shift the text */ + if (cell_idx + ime_seat->ime.preedit.cursor.start > glyph_offset) + cell_idx += ime_seat->ime.preedit.cursor.end; + else + cell_idx += ime_seat->ime.preedit.cursor.start; + } + } +#endif + + if (cell_idx < glyph_offset) { + /* Shift to the *left*, making *this* character the + * *first* visible one */ + term->render.search_glyph_offset = glyph_offset = cell_idx; + } + + else if (cell_idx > glyph_offset + visible_cells) { + /* Shift to the *right*, making *this* character the + * *last* visible one */ + term->render.search_glyph_offset = glyph_offset = + cell_idx - min(cell_idx, visible_cells); + } + + /* Adjust offset if there is free space available */ + if (total_cells - glyph_offset < visible_cells) { + term->render.search_glyph_offset = glyph_offset = + total_cells - min(total_cells, visible_cells); + } + + break; + } + + /* Ensure offset is at a character boundary */ + for (size_t i = 0, cell_idx = 0; i <= text_len; cell_idx += widths[i], i++) { + if (cell_idx >= glyph_offset) { + term->render.search_glyph_offset = glyph_offset = cell_idx; + break; + } + } + + /* + * Render the search string, starting at 'glyph_offset'. Note that + * glyph_offset is in cells, not characters + */ + for (size_t i = 0, + cell_idx = 0, + width = widths[i], + next_cell_idx = width; + i < text_len; + i++, + cell_idx = next_cell_idx, + width = widths[i], + next_cell_idx += width) + { + /* Convert subsurface coordinates to window coordinates*/ + /* Render cursor */ + if (i == term->search.cursor) { +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + bool have_preedit = + ime_seat != NULL && ime_seat->ime.preedit.cells != NULL; + bool hidden = + ime_seat != NULL && ime_seat->ime.preedit.cursor.hidden; + + if (have_preedit && !hidden) { + /* Cursor may be outside the visible area: + * cell_idx-glyph_offset can be negative */ + int cells_left = visible_cells - max( + (ssize_t)(cell_idx - glyph_offset), 0); + + /* If cursor is outside the visible area, we need to + * adjust our rectangle's position */ + int start = ime_seat->ime.preedit.cursor.start + + min((ssize_t)(cell_idx - glyph_offset), 0); + int end = ime_seat->ime.preedit.cursor.end + + min((ssize_t)(cell_idx - glyph_offset), 0); + + if (start == end) { + int count = min(ime_seat->ime.preedit.count, cells_left); + + /* Underline the entire (visible part of) pre-edit text */ + draw_underline(term, buf->pix[0], font, &fg, x, y, count); + + /* Bar-styled cursor, if in the visible area */ + if (start >= 0 && start <= visible_cells) { + draw_beam_cursor( + term, buf->pix[0], font, &fg, + x + start * term->cell_width, y); + } + + term_ime_set_cursor_rect(term, + WINDOW_X(x + start * term->cell_width), WINDOW_Y(y), + 1, term->cell_height); + } else { + /* Underline everything before and after the cursor */ + int count1 = min(start, cells_left); + int count2 = max( + min(ime_seat->ime.preedit.count - ime_seat->ime.preedit.cursor.end, + cells_left - end), + 0); + draw_underline(term, buf->pix[0], font, &fg, x, y, count1); + draw_underline(term, buf->pix[0], font, &fg, x + end * term->cell_width, y, count2); + + /* TODO: how do we handle a partially hidden rectangle? */ + if (start >= 0 && end <= visible_cells) { + draw_hollow_block( + term, buf->pix[0], &fg, x + start * term->cell_width, y, end - start); + } + term_ime_set_cursor_rect(term, + WINDOW_X(x + start * term->cell_width), WINDOW_Y(y), + term->cell_width * (end - start), term->cell_height); + } + } else if (!have_preedit) +#endif + { + /* Cursor *should* be in the visible area */ + xassert(cell_idx >= glyph_offset); + xassert(cell_idx <= glyph_offset + visible_cells); + draw_beam_cursor(term, buf->pix[0], font, &fg, x, y); + term_ime_set_cursor_rect( + term, WINDOW_X(x), WINDOW_Y(y), 1, term->cell_height); + } + } + + if (next_cell_idx >= glyph_offset && next_cell_idx - glyph_offset > visible_cells) { + /* We're now beyond the visible area - nothing more to render */ + break; + } + + if (cell_idx < glyph_offset) { + /* We haven't yet reached the visible part of the string */ + cell_idx = next_cell_idx; + continue; + } + + const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( + font, text[i], term->font_subpixel); + + if (glyph == NULL) { + cell_idx = next_cell_idx; + continue; + } + + if (unlikely(glyph->is_color_glyph)) { + pixman_image_composite32( + PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, + x + x_ofs + glyph->x, y + term->font_baseline - glyph->y, + glyph->width, glyph->height); + } else { + int combining_ofs = width == 0 + ? (glyph->x < 0 + ? width * term->cell_width + : (width - 1) * term->cell_width) + : 0; /* Not a zero-width character - no additional offset */ + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + pixman_image_composite32( + PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, + x + x_ofs + combining_ofs + glyph->x, + y + term->font_baseline - glyph->y, + glyph->width, glyph->height); + pixman_image_unref(src); + } + + x += width * term->cell_width; + cell_idx = next_cell_idx; + } + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (ime_seat != NULL && ime_seat->ime.preedit.cells != NULL) + /* Already rendered */; + else +#endif + if (term->search.cursor >= term->search.len) { + draw_beam_cursor(term, buf->pix[0], font, &fg, x, y); + term_ime_set_cursor_rect( + term, WINDOW_X(x), WINDOW_Y(y), 1, term->cell_height); + } + + /* Status string (toggles + counter) immediately to the left of + * the close glyph */ + { + int sx = (int)width - (int)margin - (int)close_w + - (int)close_gap - (int)status_w; + const int sy = (int)margin; + for (size_t i = 0; i < st_len; i++) { + const struct fcft_glyph *g = fcft_rasterize_char_utf32( + font, status[i], term->font_subpixel); + int w = max(1, c32width(status[i])); + if (g != NULL && !g->is_color_glyph) { + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + pixman_image_composite32( + PIXMAN_OP_OVER, src, g->pix, buf->pix[0], 0, 0, 0, 0, + sx + x_ofs + g->x, + sy + term->font_baseline - g->y, + g->width, g->height); + pixman_image_unref(src); + } + sx += w * (int)term->cell_width; + } + } + + /* Close glyph (×) at the top-right of the popup */ + { + const struct fcft_glyph *cg = fcft_rasterize_char_utf32( + font, U'×', term->font_subpixel); + if (cg != NULL) { + const int cx = (int)width - (int)margin - (int)close_w; + const int cy = (int)margin; + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + pixman_image_composite32( + PIXMAN_OP_OVER, src, cg->pix, buf->pix[0], 0, 0, 0, 0, + cx + x_ofs + cg->x, + cy + term->font_baseline - cg->y, + cg->width, cg->height); + pixman_image_unref(src); + } + } + + quirk_weston_subsurface_desync_on(term->window->search.sub); + + /* TODO: this is only necessary on a window resize */ + wl_subsurface_set_position( + term->window->search.sub, + (int32_t)roundf(max(0, (int32_t)term->width - (int32_t)width - (int32_t)outer_margin) / scale), + (int32_t)roundf(outer_margin / scale)); + + wayl_surface_scale(term->window, &term->window->search.surface, buf, scale); + wl_surface_attach(term->window->search.surface.surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(term->window->search.surface.surf, 0, 0, width, height); + + struct wl_region *region = wl_compositor_create_region(term->wl->compositor); + if (region != NULL) { + wl_region_add(region, 0, 0, width, height); + wl_surface_set_opaque_region(term->window->search.surface.surf, region); + wl_region_destroy(region); + } + + wl_surface_commit(term->window->search.surface.surf); + quirk_weston_subsurface_desync_off(term->window->search.sub); + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + free(text); +#endif +#undef WINDOW_X +#undef WINDOW_Y +} + +static void +render_urls(struct terminal *term) +{ + struct wl_window *win = term->window; + xassert(tll_length(win->urls) > 0); + + const float scale = term->scale; + const int x_margin = (int)roundf(2 * scale); + const int y_margin = (int)roundf(1 * scale); + + /* Calculate view start, counted from the *current* scrollback start */ + const int scrollback_end + = (term->grid->offset + term->rows) & (term->grid->num_rows - 1); + const int view_start + = (term->grid->view + - scrollback_end + + term->grid->num_rows) & (term->grid->num_rows - 1); + const int view_end = view_start + term->rows - 1; + + const bool show_url = term->urls_show_uri_on_jump_label; + + /* + * There can potentially be a lot of URLs. + * + * Since each URL is a separate sub-surface, and requires its own + * SHM buffer, we may be allocating a lot of buffers. + * + * SHM buffers normally have their own, private SHM buffer + * pool. Each pool is mmapped, and thus allocates *at least* + * 4K. Since URL labels are typically small, we end up using an + * excessive amount of both virtual and physical memory. + * + * For this reason, we instead use shm_get_many(), which uses a + * single, shared pool for all buffers. + * + * To be able to use it, we need to have all the *all* the buffer + * dimensions up front. + * + * Thus, the first iteration through the URLs do the heavy + * lifting: builds the label contents and calculates both its + * position and size. But instead of rendering the label + * immediately, we store the calculated data, and then do a second + * pass, where we first get all our buffers, and then render to + * them. + */ + + /* Positioning data + label contents */ + struct { + const struct wl_url *url; + char32_t *text; + int x; + int y; + } info[tll_length(win->urls)]; + + /* For shm_get_many() */ + int widths[tll_length(win->urls)]; + int heights[tll_length(win->urls)]; + + size_t render_count = 0; + + tll_foreach(win->urls, it) { + const struct url *url = it->item.url; + const char32_t *key = url->key; + const size_t entered_key_len = c32len(term->url_keys); + + if (key == NULL) { + /* TODO: if we decide to use the .text field, we cannot + * just skip the entire jump label like this */ + continue; + } + + struct wl_surface *surf = it->item.surf.surface.surf; + struct wl_subsurface *sub_surf = it->item.surf.sub; + + if (surf == NULL || sub_surf == NULL) + continue; + + bool hide = false; + const struct coord *pos = &url->range.start; + const int _row + = (pos->row + - scrollback_end + + term->grid->num_rows) & (term->grid->num_rows - 1); + + if (_row < view_start || _row > view_end) + hide = true; + if (c32len(key) <= entered_key_len) + hide = true; + if (c32ncasecmp(term->url_keys, key, entered_key_len) != 0) + hide = true; + + if (hide) { + wl_surface_attach(surf, NULL, 0, 0); + wl_surface_commit(surf); + continue; + } + + int col = pos->col; + int row = pos->row - term->grid->view; + while (row < 0) + row += term->grid->num_rows; + row &= (term->grid->num_rows - 1); + + /* Position label slightly above and to the left */ + int x = col * term->cell_width - 15 * term->cell_width / 10; + int y = row * term->cell_height - 5 * term->cell_height / 10; + + /* Don't position it outside our window */ + if (x < -term->margins.left) + x = -term->margins.left; + if (y < -term->margins.top) + y = -term->margins.top; + + /* Maximum width of label, in pixels */ + const int max_width = + term->width - term->margins.left - term->margins.right - x; + const int max_cols = max_width / term->cell_width; + + const size_t key_len = c32len(key); + + size_t url_len = mbstoc32(NULL, url->url, 0); + if (url_len == (size_t)-1) + url_len = 0; + + char32_t url_wchars[url_len + 1]; + mbstoc32(url_wchars, url->url, url_len + 1); + + /* Format label, not yet subject to any size limitations */ + size_t chars = key_len + (show_url ? (2 + url_len) : 0); + char32_t label[chars + 1]; + label[chars] = U'\0'; + + if (show_url) { + c32cpy(label, key); + c32cat(label, U": "); + c32cat(label, url_wchars); + } else + c32ncpy(label, key, chars); + + /* Upper case the key characters */ + for (size_t i = 0; i < c32len(key); i++) + label[i] = toc32upper(label[i]); + + /* Blank already entered key characters */ + for (size_t i = 0; i < entered_key_len; i++) + label[i] = U' '; + + /* + * Don't extend outside our window + * + * Truncate label so that it doesn't extend outside our + * window. + * + * Do it in a way such that we don't cut the label in the + * middle of a double-width character. + */ + + int cols = 0; + + for (size_t i = 0; i <= c32len(label); i++) { + int _cols = c32swidth(label, i); + + if (_cols == (size_t)-1) + continue; + + if (_cols >= max_cols) { + if (i > 0) + label[i - 1] = U'…'; + label[i] = U'\0'; + cols = max_cols; + break; + } + cols = _cols; + } + + if (cols == 0) + continue; + + int width = x_margin + cols * term->cell_width + x_margin; + int height = y_margin + term->cell_height + y_margin; + + width = roundf(scale * ceilf(width / scale)); + height = roundf(scale * ceilf(height / scale)); + + info[render_count].url = &it->item; + info[render_count].text = xc32dup(label); + info[render_count].x = x; + info[render_count].y = y; + + widths[render_count] = width; + heights[render_count] = height; + + render_count++; + } + + struct buffer_chain *chain = term->render.chains.url; + struct buffer *bufs[render_count]; + shm_get_many(chain, render_count, widths, heights, bufs); + + uint32_t fg = term->conf->colors_dark.use_custom.jump_label + ? term->conf->colors_dark.jump_label.fg + : term->colors.table[0]; + uint32_t bg = term->conf->colors_dark.use_custom.jump_label + ? term->conf->colors_dark.jump_label.bg + : term->colors.table[3]; + + for (size_t i = 0; i < render_count; i++) { + const struct wayl_sub_surface *sub_surf = &info[i].url->surf; + + const char32_t *label = info[i].text; + const int x = info[i].x; + const int y = info[i].y; + + xassert(sub_surf->surface.surf != NULL); + xassert(sub_surf->sub != NULL); + + wl_subsurface_set_position( + sub_surf->sub, + roundf((term->margins.left + x) / scale), + roundf((term->margins.top + y) / scale)); + + render_osd( + term, sub_surf, term->fonts[0], bufs[i], label, + fg, 0xffu << 24 | bg, x_margin); + + free(info[i].text); + } +} + +static void +render_update_title(struct terminal *term) +{ + static const size_t max_len = 2048; + + const char *title = term->window_title != NULL ? term->window_title : "foot"; + char *copy = NULL; + + if (strlen(title) > max_len) { + copy = xstrndup(title, max_len); + title = copy; + } + + xdg_toplevel_set_title(term->window->xdg_toplevel, title); + free(copy); +} + +static void +frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) +{ + struct terminal *term = data; + + xassert(term->window->frame_callback == wl_callback); + wl_callback_destroy(wl_callback); + term->window->frame_callback = NULL; + + bool grid = term->render.pending.grid; + bool csd = term->render.pending.csd; + bool search = term->is_searching && term->render.pending.search; + bool urls = urls_mode_is_active(term) > 0 && term->render.pending.urls; + + term->render.pending.grid = false; + term->render.pending.csd = false; + term->render.pending.search = false; + term->render.pending.urls = false; + + struct grid *original_grid = term->grid; + if (urls_mode_is_active(term)) { + xassert(term->url_grid_snapshot != NULL); + term->grid = term->url_grid_snapshot; + } + + if (csd && term->window->csd_mode == CSD_YES) { + quirk_weston_csd_on(term); + render_csd(term); + quirk_weston_csd_off(term); + } + + if (search) + render_search_box(term); + + if (urls) + render_urls(term); + + if (term->conf->tabs.enabled && tab_overview_is_active(term->window)) + render_tab_overview(term); + + if ((grid && !term->delayed_render_timer.is_armed) || (csd | search | urls)) + grid_render(term); + + tll_foreach(term->wl->seats, it) { + if (it->item.ime_focus == term) + ime_update_cursor_rect(&it->item); + } + + term->grid = original_grid; +} + +static void +tiocswinsz(struct terminal *term) +{ + if (term->ptmx >= 0) { + if (ioctl(term->ptmx, (unsigned int)TIOCSWINSZ, + &(struct winsize){ + .ws_row = term->rows, + .ws_col = term->cols, + .ws_xpixel = term->cols * term->cell_width, + .ws_ypixel = term->rows * term->cell_height}) < 0) + { + LOG_ERRNO("TIOCSWINSZ"); + } + + term_send_size_notification(term); + } +} + +static void +delayed_reflow_of_normal_grid(struct terminal *term) +{ + if (term->interactive_resizing.grid == NULL) + return; + + xassert(term->interactive_resizing.new_rows > 0); + + struct coord *const tracking_points[] = { + &term->selection.coords.start, + &term->selection.coords.end, + }; + + /* Reflow the original (since before the resize was started) grid, + * to the *current* dimensions */ + grid_resize_and_reflow( + term->interactive_resizing.grid, term, + term->interactive_resizing.new_rows, term->normal.num_cols, + term->interactive_resizing.old_screen_rows, term->rows, + term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, + tracking_points); + + /* Replace the current, truncated, "normal" grid with the + * correctly reflowed one */ + grid_free(&term->normal); + term->normal = *term->interactive_resizing.grid; + free(term->interactive_resizing.grid); + + term->hide_cursor = term->interactive_resizing.old_hide_cursor; + + /* Reset */ + term->interactive_resizing.grid = NULL; + term->interactive_resizing.old_screen_rows = 0; + term->interactive_resizing.new_rows = 0; + term->interactive_resizing.old_hide_cursor = false; + + /* Invalidate render pointers */ + render_wait_for_preapply_damage(term); + shm_unref(term->render.last_buf); + term->render.last_buf = NULL; + term->render.last_cursor.row = NULL; + + tll_free(term->normal.scroll_damage); + sixel_reflow_grid(term, &term->normal); + + if (term->grid == &term->normal) { + term_damage_view(term); + render_refresh(term); + } + + term_ptmx_resume(term); +} + +static bool +fdm_tiocswinsz(struct fdm *fdm, int fd, int events, void *data) +{ + struct terminal *term = data; + + if (events & EPOLLIN) { + tiocswinsz(term); + delayed_reflow_of_normal_grid(term); + } + + if (term->window->resize_timeout_fd >= 0) { + fdm_del(fdm, term->window->resize_timeout_fd); + term->window->resize_timeout_fd = -1; + } + return true; +} + +static void +send_dimensions_to_client(struct terminal *term) +{ + struct wl_window *win = term->window; + + if (!win->is_resizing || term->conf->resize_delay_ms == 0) { + /* Send new dimensions to client immediately */ + tiocswinsz(term); + delayed_reflow_of_normal_grid(term); + + /* And make sure to reset and deallocate a lingering timer */ + if (win->resize_timeout_fd >= 0) { + fdm_del(term->fdm, win->resize_timeout_fd); + win->resize_timeout_fd = -1; + } + } else { + /* Send new dimensions to client "in a while" */ + assert(win->is_resizing && term->conf->resize_delay_ms > 0); + + int fd = win->resize_timeout_fd; + uint16_t delay_ms = term->conf->resize_delay_ms; + bool successfully_scheduled = false; + + if (fd < 0) { + /* Lazy create timer fd */ + fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd < 0) + LOG_ERRNO("failed to create TIOCSWINSZ timer"); + else if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_tiocswinsz, term)) { + close(fd); + fd = -1; + } + + win->resize_timeout_fd = fd; + } + + if (fd >= 0) { + /* Reset timeout */ + const struct itimerspec timeout = { + .it_value = { + .tv_sec = delay_ms / 1000, + .tv_nsec = (delay_ms % 1000) * 1000000, + }, + }; + + if (timerfd_settime(fd, 0, &timeout, NULL) < 0) { + LOG_ERRNO("failed to arm TIOCSWINSZ timer"); + fdm_del(term->fdm, fd); + win->resize_timeout_fd = -1; + } else + successfully_scheduled = true; + } + + if (!successfully_scheduled) { + tiocswinsz(term); + delayed_reflow_of_normal_grid(term); + } + } +} + +static void +set_size_from_grid(struct terminal *term, int *width, int *height, int cols, int rows) +{ + int new_width, new_height; + + /* Nominal grid dimensions */ + new_width = cols * term->cell_width; + new_height = rows * term->cell_height; + + /* Include any configured padding */ + new_width += (term->conf->pad_left + term->conf->pad_right) * term->scale; + new_height += (term->conf->pad_top + term->conf->pad_bottom) * term->scale; + + /* Round to multiples of scale */ + new_width = round(term->scale * round(new_width / term->scale)); + new_height = round(term->scale * round(new_height / term->scale)); + + if (width != NULL) + *width = new_width; + if (height != NULL) + *height = new_height; +} + +/* Move to terminal.c? */ +bool +render_resize(struct terminal *term, int width, int height, uint8_t opts) +{ + if (term->shutdown.in_progress) + return false; + + if (!term->window->is_configured) + return false; + + if (term->cell_width == 0 && term->cell_height == 0) + return false; + + const bool is_floating = + !term->window->is_maximized && + !term->window->is_fullscreen && + !term->window->is_tiled; + + /* Convert logical size to physical size */ + const float scale = term->scale; + width = round(width * scale); + height = round(height * scale); + + /* If the grid should be kept, the size should be overridden */ + if (is_floating && (opts & RESIZE_KEEP_GRID)) { + set_size_from_grid(term, &width, &height, term->cols, term->rows); + } + + if (width == 0) { + /* The compositor is letting us choose the width */ + if (term->stashed_width != 0) { + /* If a default size is requested, prefer the "last used" size */ + width = term->stashed_width; + } else { + /* Otherwise, use a user-configured size */ + switch (term->conf->size.type) { + case CONF_SIZE_PX: + width = term->conf->size.width; + + if (wayl_win_csd_borders_visible(term->window)) + width -= 2 * term->conf->csd.border_width_visible; + + width *= scale; + break; + + case CONF_SIZE_CELLS: + set_size_from_grid(term, &width, NULL, + term->conf->size.width, term->conf->size.height); + break; + } + } + } + + if (height == 0) { + /* The compositor is letting us choose the height */ + if (term->stashed_height != 0) { + /* If a default size is requested, prefer the "last used" size */ + height = term->stashed_height; + } else { + /* Otherwise, use a user-configured size */ + switch (term->conf->size.type) { + case CONF_SIZE_PX: + height = term->conf->size.height; + + /* Take CSDs into account */ + if (wayl_win_csd_titlebar_visible(term->window)) + height -= term->conf->csd.title_height; + + if (wayl_win_csd_borders_visible(term->window)) + height -= 2 * term->conf->csd.border_width_visible; + + height *= scale; + break; + + case CONF_SIZE_CELLS: + set_size_from_grid(term, NULL, &height, + term->conf->size.width, term->conf->size.height); + break; + } + } + } + + /* Don't shrink grid too much */ + const int min_cols = 1; + const int min_rows = 1; + + /* Minimum window size (must be divisible by the scaling factor)*/ + const int min_width = roundf(scale * ceilf((min_cols * term->cell_width) / scale)); + const int min_height = roundf(scale * ceilf((min_rows * term->cell_height) / scale)); + + width = max(width, min_width); + height = max(height, min_height); + + /* Padding */ + const int max_pad_x = (width - min_width) / 2; + const int max_pad_y = (height - min_height) / 2; + /* Total bar height = pill height + margin (margin only used in floating layout) */ + const uint16_t tabs_bar_logical = + term->conf->tabs.enabled + ? (term->conf->tabs.height + + (term->conf->tabs.layout == CONF_TABS_LAYOUT_FLOATING + ? term->conf->tabs.margin : 0)) + : 0; + const int conf_pad_top = term->conf->pad_top + + (term->conf->tabs.enabled && + term->conf->tabs.position == CONF_TABS_POSITION_TOP + ? tabs_bar_logical : 0); + const int conf_pad_bottom = term->conf->pad_bottom + + (term->conf->tabs.enabled && + term->conf->tabs.position == CONF_TABS_POSITION_BOTTOM + ? tabs_bar_logical : 0); + const int pad_left = min(max_pad_x, scale * term->conf->pad_left); + const int pad_right = min(max_pad_x, scale * term->conf->pad_right); + const int pad_top = min(max_pad_y, scale * conf_pad_top); + const int pad_bottom= min(max_pad_y, scale * conf_pad_bottom); + + if (is_floating && + (opts & RESIZE_BY_CELLS) && + term->conf->resize_by_cells) + { + /* If resizing in cell increments, restrict the width and height */ + width = ((width - (pad_left + pad_right)) / term->cell_width) + * term->cell_width + (pad_left + pad_right); + width = max(min_width, roundf(scale * roundf(width / scale))); + + height = ((height - (pad_top + pad_bottom)) / term->cell_height) + * term->cell_height + (pad_top + pad_bottom); + height = max(min_height, roundf(scale * roundf(height / scale))); + } + + if (!(opts & RESIZE_FORCE) && + width == term->width && + height == term->height && + scale == term->scale) + { + return false; + } + + /* Cancel an application initiated "Synchronized Update" */ + term_disable_app_sync_updates(term); + + /* Drop out of URL mode */ + urls_reset(term); + + LOG_DBG("resized: size=%dx%d (scale=%.2f)", width, height, term->scale); + term->width = width; + term->height = height; + + /* Screen rows/cols before resize */ + int old_cols = term->cols; + int old_rows = term->rows; + + /* Screen rows/cols after resize */ + const int new_cols = + (term->width - (pad_left + pad_right)) / term->cell_width; + const int new_rows = + (term->height - (pad_top + pad_bottom)) / term->cell_height; + + /* + * Requirements for scrollback: + * + * a) total number of rows (visible + scrollback history) must be + * a power of two + * b) must be representable in a plain int (signed) + * + * This means that on a "normal" system, where ints are 32-bit, + * the largest possible scrollback size is 1073741824 (0x40000000, + * 1 << 30). + * + * The largest *signed* int is 2147483647 (0x7fffffff), which is + * *not* a power of two. + * + * Note that these are theoretical limits. Most of the time, + * you'll get a memory allocation failure when trying to allocate + * the grid array. + */ + const unsigned max_scrollback = (INT_MAX >> 1) + 1; + const unsigned scrollback_lines_not_yet_power_of_two = + min((uint64_t)term->render.scrollback_lines + new_rows - 1, max_scrollback); + + /* Grid rows/cols after resize */ + const int new_normal_grid_rows = + min(1u << (32 - __builtin_clz(scrollback_lines_not_yet_power_of_two)), + max_scrollback); + const int new_alt_grid_rows = + min(1u << (32 - __builtin_clz(new_rows)), max_scrollback); + + LOG_DBG("grid rows: %d", new_normal_grid_rows); + + xassert(new_cols >= 1); + xassert(new_rows >= 1); + + /* Margins */ + const int grid_width = new_cols * term->cell_width; + const int grid_height = new_rows * term->cell_height; + const int total_x_pad = term->width - grid_width; + const int total_y_pad = term->height - grid_height; + + const enum center_when center = term->conf->center_when; + const bool centered_padding = + center == CENTER_ALWAYS || + (center == CENTER_MAXIMIZED_AND_FULLSCREEN && + (term->window->is_fullscreen || term->window->is_maximized)) || + (center == CENTER_FULLSCREEN && term->window->is_fullscreen); + + if (centered_padding && !term->window->is_resizing) { + term->margins.left = max(pad_left, min(total_x_pad - pad_right, total_x_pad / 2)); + term->margins.top = max(pad_top, min(total_y_pad - pad_bottom, total_y_pad / 2)); + } else { + term->margins.left = pad_left; + term->margins.top = pad_top; + } + term->margins.right = total_x_pad - term->margins.left; + term->margins.bottom = total_y_pad - term->margins.top; + + xassert(term->margins.left >= pad_left); + xassert(term->margins.right >= pad_right); + xassert(term->margins.top >= pad_top); + xassert(term->margins.bottom >= pad_bottom); + + if (new_cols == old_cols && new_rows == old_rows) { + LOG_DBG("grid layout unaffected; skipping reflow"); + term->interactive_resizing.new_rows = new_normal_grid_rows; + goto damage_view; + } + + + /* + * Since text reflow is slow, don't do it *while* resizing. Only + * do it when done, or after "pausing" the resize for sufficiently + * long. We reuse the TIOCSWINSZ timer to handle this. See + * send_dimensions_to_client() and fdm_tiocswinsz(). + * + * To be able to do the final reflow correctly, we need a copy of + * the original grid, before the resize started. + */ + if (term->window->is_resizing && term->conf->resize_delay_ms > 0) { + if (term->interactive_resizing.grid == NULL) { + term_ptmx_pause(term); + + /* Stash the current 'normal' grid, as-is, to be used when + * doing the final reflow */ + term->interactive_resizing.old_screen_rows = term->rows; + term->interactive_resizing.old_cols = term->cols; + term->interactive_resizing.old_hide_cursor = term->hide_cursor; + term->interactive_resizing.grid = xmalloc(sizeof(*term->interactive_resizing.grid)); + *term->interactive_resizing.grid = term->normal; + + if (term->grid == &term->normal) + term->interactive_resizing.selection_coords = term->selection.coords; + } else { + /* We'll replace the current temporary grid, with a new + * one (again based on the original grid) */ + grid_free(&term->normal); + } + + struct grid *orig = term->interactive_resizing.grid; + + /* + * Copy the current viewport (of the original grid) to a new + * grid that will be used during the resize. For now, throw + * away sixels and OSC-8 URLs. They'll be "restored" when we + * do the final reflow. + * + * Note that OSC-8 URLs are perfectly ok to throw away; they + * cannot be interacted with during the resize. And, even if + * url.osc8-underline=always, the "underline" attribute is + * part of the cell, not the URI struct (and thus our faked + * grid will still render OSC-8 links underlined). + * + * TODO: + * - sixels? + */ + struct grid g = { + .num_rows = 1 << (32 - __builtin_clz(term->interactive_resizing.old_screen_rows)), + .num_cols = term->interactive_resizing.old_cols, + .offset = 0, + .view = 0, + .cursor = orig->cursor, + .saved_cursor = orig->saved_cursor, + .rows = xcalloc(g.num_rows, sizeof(g.rows[0])), + .cur_row = NULL, + .scroll_damage = tll_init(), + .sixel_images = tll_init(), + .kitty_kbd = orig->kitty_kbd, + }; + + term->selection.coords.start.row -= orig->view; + term->selection.coords.end.row -= orig->view; + + for (size_t i = 0, j = orig->view; + i < term->interactive_resizing.old_screen_rows; + i++, j = (j + 1) & (orig->num_rows - 1)) + { + g.rows[i] = grid_row_alloc(g.num_cols, false); + memcpy(g.rows[i]->cells, + orig->rows[j]->cells, + g.num_cols * sizeof(g.rows[i]->cells[0])); + + if (orig->rows[j]->extra == NULL || + orig->rows[j]->extra->underline_ranges.count == 0) + { + continue; + } + + /* + * Copy underline ranges + */ + + const struct row_ranges *underline_src = &orig->rows[j]->extra->underline_ranges; + + const int count = underline_src->count; + g.rows[i]->extra = xcalloc(1, sizeof(*g.rows[i]->extra)); + g.rows[i]->extra->underline_ranges.v = xmalloc( + count * sizeof(g.rows[i]->extra->underline_ranges.v[0])); + + struct row_ranges *underline_dst = &g.rows[i]->extra->underline_ranges; + underline_dst->count = underline_dst->size = count; + + for (int k = 0; k < count; k++) + underline_dst->v[k] = underline_src->v[k]; + } + + term->normal = g; + term->hide_cursor = true; + } + + if (term->grid == &term->alt) + selection_cancel(term); + else { + /* + * Don't cancel, but make sure there aren't any ongoing + * selections after the resize. + */ + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) + selection_finalize(&it->item, term, it->item.pointer.serial); + } + } + + /* + * TODO: if we remove the selection_finalize() call above (i.e. if + * we start allowing selections to be ongoing across resizes), the + * selection's pivot point coordinates *must* be added to the + * tracking points list. + */ + /* Resize grids */ + if (term->window->is_resizing && term->conf->resize_delay_ms > 0) { + /* Simple truncating resize, *while* an interactive resize is + * ongoing. */ + xassert(term->interactive_resizing.grid != NULL); + xassert(new_normal_grid_rows > 0); + term->interactive_resizing.new_rows = new_normal_grid_rows; + + grid_resize_without_reflow( + &term->normal, new_alt_grid_rows, new_cols, + term->interactive_resizing.old_screen_rows, new_rows); + } else { + /* Full text reflow */ + + int old_normal_rows = old_rows; + + if (term->interactive_resizing.grid != NULL) { + /* Throw away the current, truncated, "normal" grid, and + * use the original grid instead (from before the resize + * started) */ + grid_free(&term->normal); + term->normal = *term->interactive_resizing.grid; + free(term->interactive_resizing.grid); + + term->hide_cursor = term->interactive_resizing.old_hide_cursor; + term->selection.coords = term->interactive_resizing.selection_coords; + + old_normal_rows = term->interactive_resizing.old_screen_rows; + + term->interactive_resizing.grid = NULL; + term->interactive_resizing.old_screen_rows = 0; + term->interactive_resizing.new_rows = 0; + term->interactive_resizing.old_hide_cursor = false; + term->interactive_resizing.selection_coords = (struct range){{-1, -1}, {-1, -1}}; + term_ptmx_resume(term); + } + + struct coord *const tracking_points[] = { + &term->selection.coords.start, + &term->selection.coords.end, + }; + + grid_resize_and_reflow( + &term->normal, term, new_normal_grid_rows, new_cols, old_normal_rows, new_rows, + term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, + tracking_points); + } + + grid_resize_without_reflow( + &term->alt, new_alt_grid_rows, new_cols, old_rows, new_rows); + + /* Reset tab stops */ + tll_free(term->tab_stops); + for (int c = 0; c < new_cols; c += 8) + tll_push_back(term->tab_stops, c); + + term->cols = new_cols; + term->rows = new_rows; + + sixel_reflow(term); + + LOG_DBG("resized: grid: cols=%d, rows=%d " + "(left-margin=%d, right-margin=%d, top-margin=%d, bottom-margin=%d)", + term->cols, term->rows, + term->margins.left, term->margins.right, + term->margins.top, term->margins.bottom); + + if (term->scroll_region.start >= term->rows) + term->scroll_region.start = 0; + if (term->scroll_region.end > term->rows || + term->scroll_region.end >= old_rows) + { + term->scroll_region.end = term->rows; + } + + term->render.last_cursor.row = NULL; + +damage_view: + /* Signal TIOCSWINSZ */ + send_dimensions_to_client(term); + + if (is_floating) { + /* Stash current size, to enable us to restore it when we're + * being un-maximized/fullscreened/tiled */ + term->stashed_width = term->width; + term->stashed_height = term->height; + } + + { + const bool title_shown = wayl_win_csd_titlebar_visible(term->window); + const bool border_shown = wayl_win_csd_borders_visible(term->window); + + const int title = title_shown + ? roundf(term->conf->csd.title_height * scale) + : 0; + const int border = border_shown + ? roundf(term->conf->csd.border_width_visible * scale) + : 0; + + /* Must use surface logical coordinates (same calculations as + in get_csd_data(), but with different inputs) */ + const int toplevel_min_width = roundf(border / scale) + + roundf(min_width / scale) + + roundf(border / scale); + + const int toplevel_min_height = roundf(border / scale) + + roundf(title / scale) + + roundf(min_height / scale) + + roundf(border / scale); + + const int toplevel_width = roundf(border / scale) + + roundf(term->width / scale) + + roundf(border / scale); + + const int toplevel_height = roundf(border / scale) + + roundf(title / scale) + + roundf(term->height / scale) + + roundf(border / scale); + + const int x = roundf(-border / scale); + const int y = roundf(-title / scale) - roundf(border / scale); + + xdg_toplevel_set_min_size( + term->window->xdg_toplevel, + toplevel_min_width, toplevel_min_height); + + xdg_surface_set_window_geometry( + term->window->xdg_surface, + x, y, toplevel_width, toplevel_height); + } + + tll_free(term->normal.scroll_damage); + tll_free(term->alt.scroll_damage); + + render_wait_for_preapply_damage(term); + shm_unref(term->render.last_buf); + term->render.last_buf = NULL; + term_damage_view(term); + render_refresh_csd(term); + render_refresh_search(term); + render_refresh(term); + + return true; +} + +static void xcursor_callback( + void *data, struct wl_callback *wl_callback, uint32_t callback_data); +static const struct wl_callback_listener xcursor_listener = { + .done = &xcursor_callback, +}; + +bool +render_xcursor_is_valid(const struct seat *seat, const char *cursor) +{ + if (cursor == NULL) + return false; + if (seat->pointer.theme == NULL) + return false; + return wl_cursor_theme_get_cursor(seat->pointer.theme, cursor) != NULL; +} + +static void +render_xcursor_update(struct seat *seat) +{ + /* If called from a frame callback, we may no longer have mouse focus */ + if (!seat->mouse_focus) + return; + + xassert(seat->pointer.shape != CURSOR_SHAPE_NONE); + + if (seat->pointer.shape == CURSOR_SHAPE_HIDDEN) { + /* Hide cursor */ + LOG_DBG("hiding cursor using client-side NULL-surface"); + wl_surface_attach(seat->pointer.surface.surf, NULL, 0, 0); + wl_pointer_set_cursor( + seat->wl_pointer, seat->pointer.serial, seat->pointer.surface.surf, + 0, 0); + wl_surface_commit(seat->pointer.surface.surf); + return; + } + + const enum cursor_shape shape = seat->pointer.shape; + const char *const xcursor = seat->pointer.last_custom_xcursor; + + if (seat->pointer.shape_device != NULL) { + xassert(shape != CURSOR_SHAPE_CUSTOM || xcursor != NULL); + + const enum wp_cursor_shape_device_v1_shape custom_shape = + (shape == CURSOR_SHAPE_CUSTOM && xcursor != NULL + ? cursor_string_to_server_shape( + xcursor, seat->wayl->shape_manager_version) + : 0); + + if (shape != CURSOR_SHAPE_CUSTOM || custom_shape != 0) { + xassert(custom_shape == 0 || shape == CURSOR_SHAPE_CUSTOM); + + const enum wp_cursor_shape_device_v1_shape wp_shape = custom_shape != 0 + ? custom_shape + : cursor_shape_to_server_shape(shape); + + LOG_DBG("setting %scursor shape using cursor-shape-v1", + custom_shape != 0 ? "custom " : ""); + + wp_cursor_shape_device_v1_set_shape( + seat->pointer.shape_device, + seat->pointer.serial, + wp_shape); + + return; + } + } + + LOG_DBG("setting %scursor shape using a client-side cursor surface", + seat->pointer.shape == CURSOR_SHAPE_CUSTOM ? "custom " : ""); + + if (seat->pointer.cursor == NULL) { + /* + * Normally, we never get here with a NULL-cursor, because we + * only schedule a cursor update when we succeed to load the + * cursor image. + * + * However, it is possible that we did succeed to load an + * image, and scheduled an update. But, *before* the scheduled + * update triggers, the user mvoes the pointer, and we try to + * load a new cursor image. This time failing. + * + * In this case, we have a NULL cursor, but the scheduled + * update is still scheduled. + */ + return; + } + + const float scale = seat->pointer.scale; + struct wl_cursor_image *image = seat->pointer.cursor->images[0]; + struct wl_buffer *buf = wl_cursor_image_get_buffer(image); + + wayl_surface_scale_explicit_width_height( + seat->mouse_focus->window, + &seat->pointer.surface, image->width, image->height, scale); + + wl_surface_attach(seat->pointer.surface.surf, buf, 0, 0); + + wl_pointer_set_cursor( + seat->wl_pointer, seat->pointer.serial, + seat->pointer.surface.surf, + image->hotspot_x / scale, image->hotspot_y / scale); + + wl_surface_damage_buffer( + seat->pointer.surface.surf, 0, 0, INT32_MAX, INT32_MAX); + + xassert(seat->pointer.xcursor_callback == NULL); + seat->pointer.xcursor_callback = wl_surface_frame(seat->pointer.surface.surf); + wl_callback_add_listener(seat->pointer.xcursor_callback, &xcursor_listener, seat); + + wl_surface_commit(seat->pointer.surface.surf); +} + +static void +xcursor_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) +{ + struct seat *seat = data; + + xassert(seat->pointer.xcursor_callback == wl_callback); + wl_callback_destroy(wl_callback); + seat->pointer.xcursor_callback = NULL; + + if (seat->pointer.xcursor_pending) { + render_xcursor_update(seat); + seat->pointer.xcursor_pending = false; + } +} + +static void +fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) +{ + struct renderer *renderer = data; + struct wayland *wayl = renderer->wayl; + + tll_foreach(renderer->wayl->terms, it) { + struct terminal *term = it->item; + + if (unlikely(term->shutdown.in_progress || !term->window->is_configured)) + continue; + + /* Skip inactive tabs - they process PTY data but don't render */ + if (term->window->term != term) + continue; + + bool grid = term->render.refresh.grid; + bool csd = term->render.refresh.csd; + bool search = term->is_searching && term->render.refresh.search; + bool urls = urls_mode_is_active(term) && term->render.refresh.urls; + + if (!(grid | csd | search | urls)) + continue; + + if (term->render.app_sync_updates.enabled && !(csd | search | urls)) + continue; + + term->render.refresh.grid = false; + term->render.refresh.csd = false; + term->render.refresh.search = false; + term->render.refresh.urls = false; + + if (term->window->frame_callback == NULL) { + struct grid *original_grid = term->grid; + if (urls_mode_is_active(term)) { + xassert(term->url_grid_snapshot != NULL); + term->grid = term->url_grid_snapshot; + } + + if (csd && term->window->csd_mode == CSD_YES) { + quirk_weston_csd_on(term); + render_csd(term); + quirk_weston_csd_off(term); + } + if (search) + render_search_box(term); + if (urls) + render_urls(term); + if (term->conf->tabs.enabled) + render_tab_bar(term); + if (term->conf->tabs.enabled && tab_overview_is_active(term->window)) + render_tab_overview(term); + if (grid | csd | search | urls) + grid_render(term); + + tll_foreach(term->wl->seats, it) { + if (it->item.ime_focus == term) + ime_update_cursor_rect(&it->item); + } + + term->grid = original_grid; + } else { + /* Tells the frame callback to render again */ + term->render.pending.grid |= grid; + term->render.pending.csd |= csd; + term->render.pending.search |= search; + term->render.pending.urls |= urls; + + /* The tab bar is a desync subsurface — its commits apply + * independently of the parent surface, so we can repaint it + * now instead of deferring to the frame callback (which never + * calls render_tab_bar anyway). Without this, title changes + * while a frame is in flight leave the tab label stale until + * something else dirties the grid. */ + if (grid && term->conf->tabs.enabled) + render_tab_bar(term); + if (grid && term->conf->tabs.enabled && + tab_overview_is_active(term->window)) + render_tab_overview(term); + } + } + + tll_foreach(wayl->seats, it) { + if (it->item.pointer.xcursor_pending) { + if (it->item.pointer.xcursor_callback == NULL) { + render_xcursor_update(&it->item); + it->item.pointer.xcursor_pending = false; + } else { + /* Frame callback will call render_xcursor_update() */ + } + } + } +} + +void +render_refresh_title(struct terminal *term) +{ + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return; + + struct timespec diff; + timespec_sub(&now, &term->render.title.last_update, &diff); + + if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, + }; + + timerfd_settime(term->render.title.timer_fd, 0, &timeout, NULL); + } else { + term->render.title.last_update = now; + render_update_title(term); + } + + render_refresh_csd(term); +} + +void +render_refresh_app_id(struct terminal *term) +{ + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return; + + struct timespec diff; + timespec_sub(&now, &term->render.app_id.last_update, &diff); + + if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, + }; + + timerfd_settime(term->render.app_id.timer_fd, 0, &timeout, NULL); + return; + } + + const char *app_id = + term->app_id != NULL ? term->app_id : term->conf->app_id; + + xdg_toplevel_set_app_id(term->window->xdg_toplevel, app_id); + term->render.app_id.last_update = now; +} + +void +render_refresh_icon(struct terminal *term) +{ + if (term->wl->toplevel_icon_manager == NULL) { + LOG_DBG("compositor does not implement xdg-toplevel-icon: " + "ignoring request to refresh window icon"); + return; + } + + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) + return; + + struct timespec diff; + timespec_sub(&now, &term->render.icon.last_update, &diff); + + if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { + const struct itimerspec timeout = { + .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, + }; + + timerfd_settime(term->render.icon.timer_fd, 0, &timeout, NULL); + return; + } + + const char *icon_name = term_icon(term); + LOG_DBG("setting toplevel icon: %s", icon_name); + + struct xdg_toplevel_icon_v1 *icon = + xdg_toplevel_icon_manager_v1_create_icon(term->wl->toplevel_icon_manager); + xdg_toplevel_icon_v1_set_name(icon, icon_name); + xdg_toplevel_icon_manager_v1_set_icon( + term->wl->toplevel_icon_manager, term->window->xdg_toplevel, icon); + xdg_toplevel_icon_v1_destroy(icon); + + term->render.icon.last_update = now; +} + +void +render_refresh(struct terminal *term) +{ + term->render.refresh.grid = true; +} + +void +render_refresh_csd(struct terminal *term) +{ + if (term->window->csd_mode == CSD_YES) + term->render.refresh.csd = true; +} + +void +render_refresh_search(struct terminal *term) +{ + if (term->is_searching) + term->render.refresh.search = true; +} + +void +render_refresh_urls(struct terminal *term) +{ + if (urls_mode_is_active(term)) + term->render.refresh.urls = true; +} + +void +render_refresh_tab_bar(struct terminal *term) +{ + if (term->conf->tabs.enabled) + term->render.refresh.grid = true; /* triggers full re-render which includes tab bar */ +} + +static void +draw_rounded_corner_aa(pixman_image_t *pix, const pixman_color_t *color, + int dx, int dy, int r, + int cx_inside, int cy_inside) +{ + if (r <= 0) + return; + + const int stride = (r + 3) & ~3; /* A8 stride aligned to 4 bytes */ + uint8_t *data = xcalloc(stride * r, sizeof(uint8_t)); + + /* Circle center in local (0..r, 0..r) coords of the corner tile. */ + const float cx = cx_inside > 0 ? (float)r : 0.f; + const float cy = cy_inside > 0 ? (float)r : 0.f; + const float r_f = (float)r; + + for (int py = 0; py < r; py++) { + for (int px = 0; px < r; px++) { + const float ex = (float)px + 0.5f - cx; + const float ey = (float)py + 0.5f - cy; + const float dist = sqrtf(ex * ex + ey * ey); + float cov = r_f - dist + 0.5f; + if (cov <= 0.f) + cov = 0.f; + else if (cov >= 1.f) + cov = 1.f; + data[py * stride + px] = (uint8_t)(cov * 255.f + 0.5f); + } + } + + pixman_image_t *mask = pixman_image_create_bits( + PIXMAN_a8, r, r, (uint32_t *)data, stride); + pixman_image_t *src = pixman_image_create_solid_fill(color); + pixman_image_composite32(PIXMAN_OP_OVER, + src, mask, pix, 0, 0, 0, 0, dx, dy, r, r); + pixman_image_unref(mask); + pixman_image_unref(src); + free(data); +} + +static void +draw_rounded_rect(pixman_image_t *pix, const pixman_color_t *color, + int x, int y, int w, int h, int r, unsigned corners) +{ + if (r <= 0 || w <= 0 || h <= 0) { + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x, y, w, h}); + return; + } + r = min(r, min(w / 2, h / 2)); + + const bool tl = corners & 1; + const bool tr = corners & 2; + const bool bl = corners & 4; + const bool br = corners & 8; + + /* Fill body regions. Corners handled separately if rounded. */ + /* Top band */ + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x + (tl ? r : 0), y, + w - (tl ? r : 0) - (tr ? r : 0), r}); + /* Middle band (full width) */ + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x, y + r, w, h - 2 * r}); + /* Bottom band */ + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x + (bl ? r : 0), y + h - r, + w - (bl ? r : 0) - (br ? r : 0), r}); + + /* Square corners: fill the r×r square. */ + if (!tl) + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x, y, r, r}); + if (!tr) + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x + w - r, y, r, r}); + if (!bl) + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x, y + h - r, r, r}); + if (!br) + pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, + &(pixman_rectangle16_t){x + w - r, y + h - r, r, r}); + + /* Rounded corners: anti-aliased quarter-circles. */ + if (tl) draw_rounded_corner_aa(pix, color, x, y, r, 1, 1); + if (tr) draw_rounded_corner_aa(pix, color, x + w - r, y, r, -1, 1); + if (bl) draw_rounded_corner_aa(pix, color, x, y + h - r, r, 1, -1); + if (br) draw_rounded_corner_aa(pix, color, x + w - r, y + h - r, r, -1, -1); +} + +static int +tab_label_build(char *buf, size_t bufsz, + const struct terminal *tab, size_t idx) +{ + const char *title = NULL; + if (tab->window_title_has_been_set && tab->window_title != NULL) + title = tab->window_title; + else if (tab->cwd != NULL) { + const char *slash = strrchr(tab->cwd, '/'); + title = (slash != NULL && slash[1] != '\0') ? slash + 1 : tab->cwd; + } + int len = snprintf(buf, bufsz, "%zu: %s", idx + 1, + title != NULL ? title : ""); + if (len < 0) + return 0; + if ((size_t)len >= bufsz) + len = (int)bufsz - 1; + return len; +} + +static int +tab_label_width(struct fcft_font *font, const char *buf, int len) +{ + if (font == NULL || len <= 0) + return 0; + + int total = 0; + const unsigned char *p = (const unsigned char *)buf; + const unsigned char *end = p + len; + while (p < end) { + utf8proc_int32_t cp; + utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); + if (consumed <= 0 || cp < 0) { + p++; + continue; + } + p += consumed; + + const struct fcft_glyph *g = fcft_rasterize_char_utf32( + font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); + if (g != NULL) + total += g->advance.x; + } + return total; +} + +/* Width (px) of the unread indicator + trailing gap, or 0 if disabled */ +static int +tab_unread_indicator_width(struct fcft_font *font, const struct config *conf, + bool has_unread, float scale) +{ + if (!has_unread || font == NULL || + conf->tabs.unread_indicator == NULL || + conf->tabs.unread_indicator[0] == '\0') + return 0; + + int w = tab_label_width(font, conf->tabs.unread_indicator, + (int)strlen(conf->tabs.unread_indicator)); + if (w <= 0) + return 0; + return w + (int)roundf(4 * scale); +} + +/* Draw the unread indicator at (x, baseline_y); returns the x offset to + * advance past the indicator (including trailing gap). */ +static int +render_tab_unread_indicator(pixman_image_t *pix, struct fcft_font *font, + const struct config *conf, float scale, + int x, int baseline_y, bool gamma_correct) +{ + if (font == NULL || conf->tabs.unread_indicator == NULL || + conf->tabs.unread_indicator[0] == '\0') + return 0; + + const pixman_color_t fg = color_hex_to_pixman( + conf->tabs.colors.unread_fg, gamma_correct); + + int gx = x; + const unsigned char *p = (const unsigned char *)conf->tabs.unread_indicator; + const unsigned char *end = p + strlen(conf->tabs.unread_indicator); + + while (p < end) { + utf8proc_int32_t cp; + utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); + if (consumed <= 0 || cp < 0) { + p++; + continue; + } + p += consumed; + + const struct fcft_glyph *g = fcft_rasterize_char_utf32( + font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); + if (g == NULL) + continue; + + if (pixman_image_get_format(g->pix) == PIXMAN_a8r8g8b8) { + pixman_image_composite32(PIXMAN_OP_OVER, + g->pix, NULL, pix, 0, 0, 0, 0, + gx + g->x, baseline_y - g->y, g->width, g->height); + } else { + pixman_image_t *src = pixman_image_create_solid_fill(&fg); + pixman_image_composite32(PIXMAN_OP_OVER, + src, g->pix, pix, 0, 0, 0, 0, + gx + g->x, baseline_y - g->y, g->width, g->height); + pixman_image_unref(src); + } + gx += g->advance.x; + } + return (gx - x) + (int)roundf(4 * scale); +} + +static void +render_tab_label(pixman_image_t *pix, struct fcft_font *font, + const pixman_color_t *fg, const struct terminal *tab, + size_t idx, int x, int y, int max_x, float scale) +{ + char label_buf[256]; + int label_len = tab_label_build(label_buf, sizeof(label_buf), tab, idx); + if (label_len <= 0 || font == NULL) + return; + + const int pad = (int)roundf(tab->conf->tabs.label_padding * scale); + int gx = x + pad; + const int clip_x = max_x - pad; + + const unsigned char *p = (const unsigned char *)label_buf; + const unsigned char *end = p + label_len; + while (p < end) { + utf8proc_int32_t cp; + utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); + if (consumed <= 0 || cp < 0) { + p++; + continue; + } + p += consumed; + + const struct fcft_glyph *glyph = fcft_rasterize_char_utf32( + font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); + if (glyph == NULL) + continue; + + if (gx + glyph->advance.x > clip_x) + break; + + if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { + pixman_image_composite32(PIXMAN_OP_OVER, + glyph->pix, NULL, pix, 0, 0, 0, 0, + gx + glyph->x, y - glyph->y, + glyph->width, glyph->height); + } else { + pixman_image_t *src = pixman_image_create_solid_fill(fg); + pixman_image_composite32(PIXMAN_OP_OVER, + src, glyph->pix, pix, 0, 0, 0, 0, + gx + glyph->x, y - glyph->y, + glyph->width, glyph->height); + pixman_image_unref(src); + } + gx += glyph->advance.x; + } +} + +static void +render_tab_bar(struct terminal *term) +{ + struct wl_window *win = term->window; + if (win->tab_bar.sub == NULL) + return; + + const struct config *conf = term->conf; + const float scale = term->scale; + const bool floating = (conf->tabs.layout == CONF_TABS_LAYOUT_FLOATING); + const bool pos_top = (conf->tabs.position == CONF_TABS_POSITION_TOP); + + /* pill height is always conf->tabs.height; total bar = pill + margin in floating */ + const int margin_px = floating ? (int)roundf(scale * conf->tabs.margin) : 0; + const int tab_h_px = (int)roundf(scale * conf->tabs.height); + const int total_h = tab_h_px + margin_px; + const int width = term->width; + + if (width <= 0 || total_h <= 0) + return; + + struct buffer_chain *chain = term->render.chains.tab_bar; + struct buffer *buf = shm_get_buffer(chain, width, total_h); + if (buf == NULL) + return; + + const bool gamma_correct = wayl_do_linear_blending(win->term->wl, conf); + const bool rounded = (conf->tabs.style == CONF_TABS_STYLE_ROUNDED); + const int r = rounded ? (int)roundf(scale * conf->tabs.corner_radius) : 0; + + /* Clear buffer: transparent for floating (shows terminal behind gaps), bg for span */ + const pixman_color_t transparent = {0, 0, 0, 0}; + const pixman_color_t bg_color = color_hex_to_pixman(conf->tabs.colors.bg, gamma_correct); + pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], + floating ? &transparent : &bg_color, + 1, &(pixman_rectangle16_t){0, 0, width, total_h}); + + const size_t tab_count = win->tab_count; + if (tab_count == 0) + goto commit; + + struct fcft_font *font = term->fonts[0]; + + /* Per-tab layout arrays */ + int tab_xs[TAB_MAX]; + int tab_ws[TAB_MAX]; + int tab_y, tab_h; + + const size_t n = min(tab_count, (size_t)TAB_MAX); + + if (floating) { + const int pad_px = (int)roundf(scale * conf->tabs.tab_padding); + const int max_tw = (int)roundf(scale * conf->tabs.tab_width); + const int label_pad = (int)roundf(scale * conf->tabs.label_padding); + const int min_tw = max(2 * label_pad, 1); + + /* Measure each tab's natural width (label + padding), capped at max_tw */ + int natural_w[TAB_MAX]; + int total_w = 0; + for (size_t i = 0; i < n; i++) { + char buf_s[256]; + int len = tab_label_build(buf_s, sizeof(buf_s), win->tabs[i], i); + int lw = tab_label_width(font, buf_s, len); + int iw = tab_unread_indicator_width( + font, conf, win->tabs[i]->has_unread, scale); + int w = lw + iw + 2 * label_pad; + if (w > max_tw) w = max_tw; + if (w < min_tw) w = min_tw; + natural_w[i] = w; + total_w += w; + } + const int total_pads = pad_px * ((int)n - 1); + int bar_w = total_w + total_pads; + + /* Shrink uniformly if we overflow the bar width */ + if (bar_w > width && total_w > 0) { + const int avail = max(width - total_pads, (int)n); + int used = 0; + for (size_t i = 0; i < n; i++) { + int w = (int)((int64_t)natural_w[i] * avail / total_w); + if (w < 1) w = 1; + tab_ws[i] = w; + used += w; + } + /* Distribute rounding residue to the first few tabs */ + int residue = avail - used; + for (size_t i = 0; i < n && residue > 0; i++, residue--) + tab_ws[i]++; + bar_w = avail + total_pads; + } else { + for (size_t i = 0; i < n; i++) + tab_ws[i] = natural_w[i]; + } + + int x_cur = (width - bar_w) / 2; + for (size_t i = 0; i < n; i++) { + tab_xs[i] = x_cur; + x_cur += tab_ws[i] + pad_px; + } + + tab_y = pos_top ? margin_px : 0; + tab_h = tab_h_px; + } else { + const int base = width / (int)n; + int x_cur = 0; + for (size_t i = 0; i < n; i++) { + tab_xs[i] = x_cur; + tab_ws[i] = (i + 1 == n) ? (width - x_cur) : base; + x_cur += base; + } + tab_y = 0; + tab_h = tab_h_px; + } + + /* Text baseline centered within the pill */ + const int text_baseline = tab_y + (font + ? (tab_h - (int)(font->ascent + font->descent)) / 2 + (int)font->ascent + : tab_h / 2); + + for (size_t i = 0; i < n; i++) { + struct terminal *tab = win->tabs[i]; + const bool is_active = (i == win->active_tab); + const int x = tab_xs[i]; + const int w = tab_ws[i]; + + const pixman_color_t tab_bg = color_hex_to_pixman( + is_active ? conf->tabs.colors.active_bg : conf->tabs.colors.bg, + gamma_correct); + const pixman_color_t fg_color = color_hex_to_pixman( + is_active ? conf->tabs.colors.active_fg : conf->tabs.colors.fg, + gamma_correct); + + if (rounded) { + /* Floating: all 4 corners rounded. Span: only the open edge rounded. */ + unsigned corners; + if (floating) { + corners = 0xf; /* all corners */ + } else if (pos_top) { + corners = 0x3; /* top-left + top-right */ + } else { + corners = 0xc; /* bottom-left + bottom-right */ + } + draw_rounded_rect(buf->pix[0], &tab_bg, x, tab_y, w, tab_h, r, corners); + } else { + pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &tab_bg, + 1, &(pixman_rectangle16_t){x, tab_y, w, tab_h}); + } + + /* Span: separator between inactive tabs */ + if (!floating && !is_active && i + 1 < n) { + const pixman_color_t sep = color_hex_to_pixman( + conf->tabs.colors.bg, gamma_correct); + pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &sep, + 1, &(pixman_rectangle16_t){x + w - 1, tab_y + 2, 1, tab_h - 4}); + } + + if (font != NULL) { + int label_x = x; + if (tab->has_unread) { + const int pad = (int)roundf(conf->tabs.label_padding * scale); + int adv = render_tab_unread_indicator( + buf->pix[0], font, conf, scale, + x + pad, text_baseline, gamma_correct); + label_x = x + adv; + } + render_tab_label(buf->pix[0], font, &fg_color, tab, i, + label_x, text_baseline, x + w, scale); + } + } + + /* Publish layout for hit-testing */ + for (size_t i = 0; i < n; i++) { + win->tab_layout.xs[i] = tab_xs[i]; + win->tab_layout.ws[i] = tab_ws[i]; + } + win->tab_layout.y = tab_y; + win->tab_layout.h = tab_h; + win->tab_layout.count = n; + +commit: ; + if (tab_count == 0) + win->tab_layout.count = 0; + const int y_pos = pos_top ? 0 : term->height - total_h; + wl_subsurface_set_position(win->tab_bar.sub, 0, (int)roundf(y_pos / scale)); + + wayl_surface_scale(win, &win->tab_bar.surface, buf, scale); + wl_surface_attach(win->tab_bar.surface.surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(win->tab_bar.surface.surf, 0, 0, width, total_h); + + if (!floating) { + struct wl_region *region = wl_compositor_create_region(term->wl->compositor); + if (region != NULL) { + wl_region_add(region, 0, 0, width, total_h); + wl_surface_set_opaque_region(win->tab_bar.surface.surf, region); + wl_region_destroy(region); + } + } else { + wl_surface_set_opaque_region(win->tab_bar.surface.surf, NULL); + } + + wl_surface_commit(win->tab_bar.surface.surf); +} + +/* === Tab overview === + * + * A full-window sub-surface that grids out all tabs as live downscaled + * thumbnails, animating in/out with a 400ms ease-out-cubic on scale + + * opacity. The animation duration matches libadwaita's Adw.TabOverview. + */ + +#define TAB_OVERVIEW_DURATION_NS 400000000ull + +static uint64_t +now_monotonic_ns(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec; +} + +static float +ease_out_cubic(float t) +{ + if (t <= 0.f) return 0.f; + if (t >= 1.f) return 1.f; + float u = 1.f - t; + return 1.f - u * u * u; +} + +bool +tab_overview_is_active(const struct wl_window *win) +{ + if (win == NULL) + return false; + const __typeof__(win->tab_overview_state) *s = &win->tab_overview_state; + return s->target_open || s->progress > 0.f || s->visible; +} + +void +tab_overview_toggle(struct wl_window *win) +{ + if (win == NULL || win->tab_overview.sub == NULL) + return; + + /* Snapshot current progress so the animation continues smoothly even + * if the user toggles mid-fade. */ + struct terminal *active = win->tabs[win->active_tab]; + if (active == NULL) + return; + + win->tab_overview_state.target_open = !win->tab_overview_state.target_open; + win->tab_overview_state.anim_from = win->tab_overview_state.progress; + win->tab_overview_state.anim_start_ns = now_monotonic_ns(); + win->tab_overview_state.sel_idx = win->active_tab; + win->tab_overview_state.hover_idx = -1; + + render_refresh(active); +} + +void +tab_overview_close_instant(struct wl_window *win) +{ + if (win == NULL || win->tab_overview.sub == NULL) + return; + __typeof__(win->tab_overview_state) *st = &win->tab_overview_state; + st->target_open = false; + st->progress = 0.f; + st->anim_from = 0.f; + st->anim_start_ns = 0; + st->hover_idx = -1; + + /* Hide the sub-surface immediately so the new active tab is visible + * without going through the fade-out frames. */ + if (st->visible) { + wl_surface_attach(win->tab_overview.surface.surf, NULL, 0, 0); + wl_surface_commit(win->tab_overview.surface.surf); + st->visible = false; + } + st->card_count = 0; + + struct terminal *active = win->tabs[win->active_tab]; + if (active != NULL) + render_refresh(active); +} + +void +render_refresh_tab_overview(struct terminal *term) +{ + if (term->conf->tabs.enabled) + term->render.refresh.grid = true; +} + +/* Compute the {cols, rows, card_w, card_h, gap, top_left} layout for n + * tabs in a window of (win_w, win_h) buffer pixels. The card aspect ratio + * mirrors the window's. */ +static void +overview_layout(int win_w, int win_h, size_t n, + int *out_cols, int *out_rows, + int *out_card_w, int *out_card_h, + int *out_gap, int *out_origin_x, int *out_origin_y, + int *out_title_h) +{ + if (n == 0 || win_w <= 0 || win_h <= 0) { + *out_cols = *out_rows = 0; + *out_card_w = *out_card_h = 0; + *out_gap = *out_origin_x = *out_origin_y = *out_title_h = 0; + return; + } + + const int margin = max(24, win_w / 32); + const int gap = max(12, win_w / 64); + const int title_h = max(18, win_h / 36); + + /* Pick column count that yields cards closest to window aspect ratio. + * cols = round(sqrt(n * win_w/win_h)) is the standard heuristic. */ + const float aspect = (float)win_w / (float)win_h; + int cols = (int)roundf(sqrtf((float)n * aspect)); + if (cols < 1) cols = 1; + if ((size_t)cols > n) cols = (int)n; + int rows = (int)((n + cols - 1) / cols); + + /* Compute card size that fits both axes */ + int avail_w = win_w - 2 * margin - (cols - 1) * gap; + int avail_h = win_h - 2 * margin - (rows - 1) * gap; + if (avail_w < 1) avail_w = 1; + if (avail_h < 1) avail_h = 1; + + int card_w = avail_w / cols; + int card_h_from_w = (int)((int64_t)card_w * win_h / win_w); + int card_h_avail = (avail_h - rows * title_h) / rows; + if (card_h_avail < 1) card_h_avail = 1; + + int card_h; + if (card_h_from_w <= card_h_avail) { + card_h = card_h_from_w; + } else { + card_h = card_h_avail; + card_w = (int)((int64_t)card_h * win_w / win_h); + } + if (card_w < 1) card_w = 1; + if (card_h < 1) card_h = 1; + + const int total_w = cols * card_w + (cols - 1) * gap; + const int total_h = rows * (card_h + title_h) + (rows - 1) * gap; + const int origin_x = (win_w - total_w) / 2; + const int origin_y = (win_h - total_h) / 2; + + *out_cols = cols; + *out_rows = rows; + *out_card_w = card_w; + *out_card_h = card_h; + *out_gap = gap; + *out_origin_x = origin_x; + *out_origin_y = origin_y; + *out_title_h = title_h; +} + +static void +draw_card_thumbnail(pixman_image_t *dst, struct buffer *src_buf, + int dst_x, int dst_y, int dst_w, int dst_h) +{ + if (src_buf == NULL || src_buf->pix == NULL || src_buf->pix[0] == NULL || + src_buf->width <= 0 || src_buf->height <= 0 || + dst_w <= 0 || dst_h <= 0) + return; + + pixman_image_t *src = src_buf->pix[0]; + pixman_image_set_filter(src, PIXMAN_FILTER_BILINEAR, NULL, 0); + + pixman_transform_t xform; + pixman_transform_init_scale(&xform, + pixman_double_to_fixed((double)src_buf->width / dst_w), + pixman_double_to_fixed((double)src_buf->height / dst_h)); + pixman_image_set_transform(src, &xform); + + pixman_image_composite32(PIXMAN_OP_SRC, + src, NULL, dst, + 0, 0, 0, 0, + dst_x, dst_y, dst_w, dst_h); + + /* Reset for subsequent normal use */ + pixman_image_set_transform(src, NULL); + pixman_image_set_filter(src, PIXMAN_FILTER_NEAREST, NULL, 0); +} + +static void +render_tab_overview(struct terminal *term) +{ + struct wl_window *win = term->window; + if (win->tab_overview.sub == NULL) + return; + + __typeof__(win->tab_overview_state) *st = &win->tab_overview_state; + + /* Advance animation */ + if (st->progress != (st->target_open ? 1.f : 0.f)) { + const uint64_t now = now_monotonic_ns(); + const uint64_t elapsed = now - st->anim_start_ns; + float t = (float)elapsed / (float)TAB_OVERVIEW_DURATION_NS; + if (t < 0.f) t = 0.f; + if (t > 1.f) t = 1.f; + const float eased = ease_out_cubic(t); + const float target = st->target_open ? 1.f : 0.f; + st->progress = st->anim_from + (target - st->anim_from) * eased; + if (t >= 1.f) + st->progress = target; + } + + /* Hide sub-surface entirely when fully closed */ + if (st->progress <= 0.f && !st->target_open) { + if (st->visible) { + wl_surface_attach(win->tab_overview.surface.surf, NULL, 0, 0); + wl_surface_commit(win->tab_overview.surface.surf); + st->visible = false; + } + st->card_count = 0; + return; + } + + const float scale = term->scale; + const int width = term->width; + const int height = term->height; + if (width <= 0 || height <= 0) + return; + + struct buffer_chain *chain = term->render.chains.tab_overview; + struct buffer *buf = shm_get_buffer(chain, width, height); + if (buf == NULL) + return; + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + + /* Backdrop dim: black at progress*0.55 alpha */ + { + const uint16_t dim = (uint16_t)(0xffff * (0.55f * st->progress)); + const pixman_color_t dim_color = {0, 0, 0, dim}; + pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], + &dim_color, 1, + &(pixman_rectangle16_t){0, 0, width, height}); + } + + const size_t n = min(win->tab_count, (size_t)TAB_MAX); + if (n == 0) + goto commit; + + int cols, rows, card_w, card_h, gap, origin_x, origin_y, title_h; + overview_layout(width, height, n, + &cols, &rows, &card_w, &card_h, &gap, + &origin_x, &origin_y, &title_h); + + /* Scale factor: 0.92 -> 1.0 over the animation */ + const float card_scale = 0.92f + 0.08f * st->progress; + const int sc_w = (int)roundf(card_w * card_scale); + const int sc_h = (int)roundf(card_h * card_scale); + const int sc_title_h = (int)roundf(title_h * card_scale); + + const int corner_r = max(6, (int)roundf(8.f * scale)); + + struct fcft_font *font = term->fonts[0]; + + /* Card opacity: ramps in faster than the backdrop */ + const float card_alpha = st->progress; + const uint16_t alpha16 = (uint16_t)(0xffff * card_alpha); + + /* Use the window/terminal palette to make cards feel native */ + pixman_color_t card_bg = color_hex_to_pixman(term->colors.bg, gamma_correct); + pixman_color_t card_fg = color_hex_to_pixman(term->colors.fg, gamma_correct); + /* Accent for active card: borrow active_bg from tab config if rounded + * tabs are configured, otherwise tint by mixing fg/bg */ + pixman_color_t accent = color_hex_to_pixman( + term->conf->tabs.colors.active_bg ? term->conf->tabs.colors.active_bg + : term->colors.fg, + gamma_correct); + card_bg.alpha = alpha16; + accent.alpha = alpha16; + + pixman_color_t title_bg = {0, 0, 0, (uint16_t)(0x8000 * card_alpha)}; + + st->card_count = n; + + for (size_t i = 0; i < n; i++) { + const int col = (int)(i % (size_t)cols); + const int row = (int)(i / (size_t)cols); + + /* Per-card layout (full size) */ + const int cell_x = origin_x + col * (card_w + gap); + const int cell_y = origin_y + row * (card_h + title_h + gap); + + /* Cache full-size geometry for hit-testing */ + st->cards[i].x = cell_x; + st->cards[i].y = cell_y; + st->cards[i].w = card_w; + st->cards[i].h = card_h + title_h; + + /* Scaled-around-center geometry for drawing */ + const int draw_x = cell_x + (card_w - sc_w) / 2; + const int draw_y = cell_y + (card_h + title_h - sc_h - sc_title_h) / 2; + + const bool is_active = (i == win->active_tab); + const bool is_sel = (i == st->sel_idx); + const bool is_hover = ((int)i == st->hover_idx); + + /* Card background */ + draw_rounded_rect(buf->pix[0], &card_bg, + draw_x, draw_y, sc_w, sc_h + sc_title_h, + corner_r, 0xf); + + /* Thumbnail of the tab's last rendered grid */ + struct terminal *tab = win->tabs[i]; + if (tab != NULL && sc_w > 4 && sc_h > 4) { + draw_card_thumbnail(buf->pix[0], tab->render.last_buf, + draw_x + 2, draw_y + 2, + sc_w - 4, sc_h - 4); + } + + /* Title bar strip across the bottom of the card */ + pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &title_bg, + 1, &(pixman_rectangle16_t){ + draw_x, draw_y + sc_h, sc_w, sc_title_h}); + + if (font != NULL && tab != NULL) { + const int baseline = draw_y + sc_h + + (sc_title_h - (int)(font->ascent + font->descent)) / 2 + + (int)font->ascent; + const int label_pad = max(4, (int)roundf(4.f * scale)); + render_tab_label(buf->pix[0], font, &card_fg, tab, i, + draw_x + label_pad, baseline, + draw_x + sc_w - label_pad, scale); + } + + /* Highlights (drawn on top): a 2-3 px ring */ + if (is_active || is_sel || is_hover) { + const int ring = max(2, (int)roundf(2.f * scale)) + + (is_active ? 1 : 0); + pixman_color_t ring_color = accent; + if (!is_active && is_hover) { + ring_color.alpha = (uint16_t)(0xc000 * card_alpha); + } else if (!is_active && is_sel) { + ring_color.alpha = (uint16_t)(0xa000 * card_alpha); + } + /* Top */ + pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &ring_color, + 1, &(pixman_rectangle16_t){draw_x, draw_y, sc_w, ring}); + /* Bottom */ + pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &ring_color, + 1, &(pixman_rectangle16_t){ + draw_x, draw_y + sc_h + sc_title_h - ring, sc_w, ring}); + /* Left */ + pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &ring_color, + 1, &(pixman_rectangle16_t){ + draw_x, draw_y, ring, sc_h + sc_title_h}); + /* Right */ + pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &ring_color, + 1, &(pixman_rectangle16_t){ + draw_x + sc_w - ring, draw_y, ring, sc_h + sc_title_h}); + } + } + +commit: + wl_subsurface_set_position(win->tab_overview.sub, 0, 0); + wayl_surface_scale(win, &win->tab_overview.surface, buf, scale); + wl_surface_attach(win->tab_overview.surface.surf, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(win->tab_overview.surface.surf, 0, 0, width, height); + wl_surface_set_opaque_region(win->tab_overview.surface.surf, NULL); + wl_surface_commit(win->tab_overview.surface.surf); + st->visible = true; + + /* Keep animating until we reach the target */ + if (st->progress != (st->target_open ? 1.f : 0.f)) + term->render.refresh.grid = true; +} + +int +tab_overview_hit_test(struct wl_window *win, int x_buf, int y_buf) +{ + if (win == NULL || !tab_overview_is_active(win)) + return -1; + const __typeof__(win->tab_overview_state) *s = &win->tab_overview_state; + for (size_t i = 0; i < s->card_count; i++) { + const int x = s->cards[i].x; + const int y = s->cards[i].y; + const int w = s->cards[i].w; + const int h = s->cards[i].h; + if (x_buf >= x && x_buf < x + w && y_buf >= y && y_buf < y + h) + return (int)i; + } + return -1; +} + +bool +render_xcursor_set(struct seat *seat, struct terminal *term, + enum cursor_shape shape) +{ + if (seat->pointer.theme == NULL && seat->pointer.shape_device == NULL) + return false; + + if (seat->mouse_focus == NULL) { + seat->pointer.shape = CURSOR_SHAPE_NONE; + return true; + } + + if (seat->mouse_focus != term) { + /* This terminal doesn't have mouse focus */ + return true; + } + + if (seat->pointer.shape == shape && + !(shape == CURSOR_SHAPE_CUSTOM && + !streq(seat->pointer.last_custom_xcursor, + term->mouse_user_cursor))) + { + return true; + } + + if (shape == CURSOR_SHAPE_HIDDEN) { + seat->pointer.cursor = NULL; + free(seat->pointer.last_custom_xcursor); + seat->pointer.last_custom_xcursor = NULL; + } + + else if (seat->pointer.shape_device == NULL) { + const char *const custom_xcursors[] = {term->mouse_user_cursor, NULL}; + const char *const *xcursors = shape == CURSOR_SHAPE_CUSTOM + ? custom_xcursors + : cursor_shape_to_string(shape); + + xassert(xcursors[0] != NULL); + + seat->pointer.cursor = NULL; + + for (size_t i = 0; xcursors[i] != NULL; i++) { + seat->pointer.cursor = + wl_cursor_theme_get_cursor(seat->pointer.theme, xcursors[i]); + + if (seat->pointer.cursor != NULL) { + LOG_DBG("loaded xcursor %s", xcursors[i]); + break; + } + } + + if (seat->pointer.cursor == NULL) { + LOG_ERR( + "failed to load xcursor pointer '%s', and all of its fallbacks", + xcursors[0]); + return false; + } + } else { + /* Server-side cursors - no need to load anything */ + } + + if (shape == CURSOR_SHAPE_CUSTOM) { + free(seat->pointer.last_custom_xcursor); + seat->pointer.last_custom_xcursor = + xstrdup(term->mouse_user_cursor); + } + + /* FDM hook takes care of actual rendering */ + seat->pointer.shape = shape; + seat->pointer.xcursor_pending = true; + return true; +} + +void +render_buffer_release_callback(struct buffer *buf, void *data) +{ + /* + * Called from shm.c when a buffer is released + * + * We use it to pre-apply last-frame's damage to it, when we're + * forced to double buffer (compositor doesn't release buffers + * immediately). + * + * The timeline is thus: + * 1. We render and push a new frame + * 2. Some (hopefully short) time after that, the compositor releases the previous buffer + * 3. We're called, and kick off the thread that copies the changes from (1) to the just freed buffer + * 4. Time passes.... + * 5. The compositor calls our frame callback, signalling to us that it's time to start rendering the next frame + * 6. Hopefully, our thread is already done with copying the changes, otherwise we stall, waiting for it + * 7. We render the frame as if the compositor does immediate releases. + * + * What's the gain? Reduced latency, by applying the previous + * frame's damage as soon as possible, we shorten the time it + * takes to render the frame after the frame callback. + * + * This means the compositor can, in theory, push the frame + * callback closer to the vblank deadline, and thus reduce input + * latency. Not all compositors (most, in fact?) don't adapt like + * this, unfortunately. But some allows the user to manually + * configure the deadline. + */ + struct terminal *term = data; + + if (likely(buf->age != 1)) + return; + + if (likely(!term->render.preapply_last_frame_damage)) + return; + + if (term->render.last_buf == NULL) + return; + + if (term->render.last_buf->age != 0) + return; + + if (buf->width != term->render.last_buf->width) + return; + + if (buf->height != term->render.last_buf->height) + return; + + xassert(term->render.workers.count > 0); + xassert(term->render.last_buf != NULL); + + xassert(term->render.last_buf->age == 0); + xassert(term->render.last_buf != buf); + + mtx_lock(&term->render.workers.preapplied_damage.lock); + if (term->render.workers.preapplied_damage.buf != NULL) { + mtx_unlock(&term->render.workers.preapplied_damage.lock); + return; + } + + xassert(term->render.workers.preapplied_damage.buf == NULL); + term->render.workers.preapplied_damage.buf = buf; + term->render.workers.preapplied_damage.start = (struct timespec){0}; + term->render.workers.preapplied_damage.stop = (struct timespec){0}; + mtx_unlock(&term->render.workers.preapplied_damage.lock); + + mtx_lock(&term->render.workers.lock); + sem_post(&term->render.workers.start); + xassert(tll_length(term->render.workers.queue) == 0); + tll_push_back(term->render.workers.queue, -3); + mtx_unlock(&term->render.workers.lock); +} diff --git a/render.h b/render.h new file mode 100644 index 0000000..04d4328 --- /dev/null +++ b/render.h @@ -0,0 +1,67 @@ +#pragma once +#include + +#include "terminal.h" +#include "fdm.h" +#include "wayland.h" +#include "misc.h" + +struct renderer; +struct renderer *render_init(struct fdm *fdm, struct wayland *wayl); +void render_destroy(struct renderer *renderer); + +enum resize_options { + RESIZE_NORMAL = 0, + RESIZE_FORCE = 1 << 0, + RESIZE_BY_CELLS = 1 << 1, + RESIZE_KEEP_GRID = 1 << 2, +}; + +bool render_resize( + struct terminal *term, int width, int height, uint8_t resize_options); + +void render_refresh(struct terminal *term); +void render_refresh_app_id(struct terminal *term); +void render_refresh_icon(struct terminal *term); +void render_refresh_csd(struct terminal *term); +void render_refresh_search(struct terminal *term); +void render_refresh_title(struct terminal *term); +void render_refresh_urls(struct terminal *term); +void render_refresh_tab_bar(struct terminal *term); +void render_refresh_tab_overview(struct terminal *term); + +/* Toggle the tab-overview animation on the given window. */ +void tab_overview_toggle(struct wl_window *win); + +/* Snap the overview fully closed with no closing animation. Used when + * the user activates a card (click / Enter / digit). */ +void tab_overview_close_instant(struct wl_window *win); + +/* Hit test in window-buffer pixel coords. Returns card index or -1. */ +int tab_overview_hit_test(struct wl_window *win, int x_buf, int y_buf); + +/* Whether overview is currently visible (open or animating). */ +bool tab_overview_is_active(const struct wl_window *win); +bool render_xcursor_set( + struct seat *seat, struct terminal *term, enum cursor_shape shape); +bool render_xcursor_is_valid(const struct seat *seat, const char *cursor); + +void render_overlay(struct terminal *term); + +struct render_worker_context { + int my_id; + struct terminal *term; +}; +int render_worker_thread(void *_ctx); + +struct csd_data { + int x; + int y; + int width; + int height; +}; + +struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx); + +void render_buffer_release_callback(struct buffer *buf, void *data); +void render_wait_for_preapply_damage(struct terminal *term); diff --git a/scripts/benchmark.py b/scripts/benchmark.py new file mode 100755 index 0000000..fe820d9 --- /dev/null +++ b/scripts/benchmark.py @@ -0,0 +1,51 @@ +#!/usr/bin/env -S python3 -u + +import argparse +import fcntl +import os +import statistics +import struct +import sys +import termios + +from datetime import datetime + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument('files', type=argparse.FileType('rb'), nargs='+') + parser.add_argument('--iterations', type=int, default=20) + + args = parser.parse_args() + + lines, cols, height, width = struct.unpack( + 'HHHH', + fcntl.ioctl(sys.stdout.fileno(), + termios.TIOCGWINSZ, + struct.pack('HHHH', 0, 0, 0, 0))) + + times: dict[str, list[float]] = {name: [] for name in [f.name for f in args.files]} + + for f in args.files: + bench_bytes = f.read() + + for _ in range(args.iterations): + start = datetime.now() + sys.stdout.buffer.write(bench_bytes) + stop = datetime.now() + + times[f.name].append((stop - start).total_seconds()) + + del bench_bytes + + print('\033[J') + print(times) + print(f'cols={cols}, lines={lines}, width={width}px, height={height}px') + for f in args.files: + print(f'{os.path.basename(f.name)}: ' + f'{statistics.mean(times[f.name]):.3f}s ' + f'±{statistics.stdev(times[f.name]):.3f}') + + +if __name__ == '__main__': + main() diff --git a/scripts/generate-alt-random-writes.py b/scripts/generate-alt-random-writes.py new file mode 100755 index 0000000..7ad1460 --- /dev/null +++ b/scripts/generate-alt-random-writes.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +import argparse +import enum +import fcntl +import random +import signal +import struct +import sys +import termios + +from typing import Any + + +class ColorVariant(enum.IntEnum): + NONE = enum.auto() + REGULAR = enum.auto() + BRIGHT = enum.auto() + CUBE = enum.auto() + RGB = enum.auto() + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + 'out', type=argparse.FileType(mode='w'), nargs='?', help='name of output file') + parser.add_argument('--cols', type=int) + parser.add_argument('--rows', type=int) + parser.add_argument('--colors-regular', action='store_true') + parser.add_argument('--colors-bright', action='store_true') + parser.add_argument('--colors-256', action='store_true') + parser.add_argument('--colors-rgb', action='store_true') + parser.add_argument('--scroll', action='store_true') + parser.add_argument('--scroll-region', action='store_true') + parser.add_argument('--attr-bold', action='store_true') + parser.add_argument('--attr-italic', action='store_true') + parser.add_argument('--attr-underline', action='store_true') + parser.add_argument('--sixel', action='store_true') + parser.add_argument('--seed', type=int) + + opts = parser.parse_args() + out = opts.out if opts.out is not None else sys.stdout + + lines: int | None = None + cols: int | None = None + width: int | None = None + height: int | None = None + + if opts.rows is None or opts.cols is None: + try: + def dummy(*args: Any) -> None: + """Need a handler installed for sigwait() to trigger.""" + _ = args + pass + signal.signal(signal.SIGWINCH, dummy) + + while True: + with open('/dev/tty', 'rb') as pty: + lines, cols, height, width = struct.unpack( + 'HHHH', + fcntl.ioctl(pty, + termios.TIOCGWINSZ, + struct.pack('HHHH', 0, 0, 0, 0))) + + assert width is not None + assert height is not None + + if width > 0 and height > 0: + break + + # We’re early; the foot window hasn’t been mapped yet. Or, + # to be more precise, fonts haven’t yet been loaded, + # meaning it doesn’t have any cell geometry yet. + signal.sigwait([signal.SIGWINCH]) + + signal.signal(signal.SIGWINCH, signal.SIG_DFL) + + except OSError: + lines = None + cols = None + height = None + width = None + + if opts.rows is not None: + lines = opts.rows + assert lines is not None + height = 15 * lines # PGO helper binary hardcodes cell height to 15px + if opts.cols is not None: + cols = opts.cols + assert cols is not None + width = 8 * cols # PGO help binary hardcodes cell width to 8px + + if lines is None or cols is None or height is None or width is None: + raise Exception('could not get terminal width/height; use --rows and --cols') + + assert lines > 0, f'{lines}' + assert cols > 0, f'{cols}' + assert width > 0, f'{width}' + assert height > 0, f'{height}' + + # Number of characters to write to screen + count = 256 * 1024**1 + + # Characters to choose from + alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRTSTUVWXYZ0123456789 öäå 👨👩🧒👩🏽‍🔬🇸🇪' + + color_variants = ([ColorVariant.NONE] + + ([ColorVariant.REGULAR] if opts.colors_regular else []) + + ([ColorVariant.BRIGHT] if opts.colors_bright else []) + + ([ColorVariant.CUBE] if opts.colors_256 else []) + + ([ColorVariant.RGB] if opts.colors_rgb else [])) + + # Enter alt screen + out.write('\033[?1049h') + + # uses system time or /dev/urandom if available if opt.seed == None + # pin seeding method to make seeding stable across future versions + random.seed(a=opts.seed, version=2) + + for _ in range(count): + if opts.scroll and random.randrange(256) == 0: + out.write('\033[m') + + if opts.scroll_region and random.randrange(256) == 0: + top = random.randrange(3) + bottom = random.randrange(3) + out.write(f'\033[{top};{lines - bottom}r') + + lines_to_scroll = random.randrange(lines - 1) + rev = random.randrange(2) + if not rev and random.randrange(2): + out.write(f'\033[{lines};{cols}H') + out.write('\n' * lines_to_scroll) + else: + out.write(f'\033[{lines_to_scroll + 1}{"T" if rev == 1 else "S"}') + continue + + # Generate a random location and a random character + row = random.randrange(lines) + col = random.randrange(cols) + c = random.choice(alphabet) + + repeat = random.randrange((cols - col) + 1) + assert col + repeat <= cols + + color_variant = random.choice(color_variants) + + # Position cursor + out.write(f'\033[{row + 1};{col + 1}H') + + if color_variant in [ColorVariant.REGULAR, ColorVariant.BRIGHT]: + do_bg = random.randrange(2) + base = 40 if do_bg else 30 + base += 60 if color_variant == ColorVariant.BRIGHT else 0 + + idx = random.randrange(8) + out.write(f'\033[{base + idx}m') + + elif color_variant == ColorVariant.CUBE: + do_bg = random.randrange(2) + base = 48 if do_bg else 38 + + idx = random.randrange(256) + if random.randrange(2): + # Old-style + out.write(f'\033[{base};5;{idx}m') + else: + # New-style (sub-parameter based) + out.write(f'\033[{base}:5:{idx}m') + + elif color_variant == ColorVariant.RGB: + do_bg = random.randrange(2) + base = 48 if do_bg else 38 + + # use list comprehension in favor of randbytes(n) + # which is only available for Python >= 3.9 + rgb = [random.randrange(256) for _ in range(3)] + + if random.randrange(2): + # Old-style + out.write(f'\033[{base};2;{rgb[0]};{rgb[1]};{rgb[2]}m') + else: + # New-style (sub-parameter based) + out.write(f'\033[{base}:2::{rgb[0]}:{rgb[1]}:{rgb[2]}m') + + if opts.attr_bold and random.randrange(5) == 0: + out.write('\033[1m') + if opts.attr_italic and random.randrange(5) == 0: + out.write('\033[3m') + if opts.attr_underline and random.randrange(5) == 0: + out.write('\033[4m') + + out.write(c * repeat) + + do_sgr_reset = random.randrange(2) + if do_sgr_reset: + reset_actions = ['\033[m', '\033[39m', '\033[49m'] + out.write(random.choice(reset_actions)) + + # Reset colors + out.write('\033[m\033[r') + + if opts.sixel: + # The sixel 'alphabet' + sixels = '?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~' + + last_pos: tuple[int, int] | None = None + last_size: tuple[int, int] = 0, 0 + + for _ in range(20): + if last_pos is not None and random.randrange(2): + # Overwrite last sixel. I.e. use same position and + # size as last sixel + pass + else: + # Random origin in upper left quadrant + last_pos = random.randrange(lines // 2) + 1, random.randrange(cols // 2) + 1 + last_size = random.randrange((height + 1) // 2), random.randrange((width + 1) // 2) + + out.write(f'\033[{last_pos[0]};{last_pos[1]}H') + six_height, six_width = last_size + six_rows = (six_height + 5) // 6 # Round up; each sixel is 6 pixels + + # Begin sixel (with P2 set to either 0 or 1 - opaque or transparent) + sixel_p2 = random.randrange(2) + out.write(f'\033P;{sixel_p2}q') + + # Sixel size. Without this, sixels will be + # auto-resized on cell-boundaries. + out.write(f'"1;1;{six_width};{six_height}') + + # Set up 256 random colors + for idx in range(256): + # param 2: 1=HLS, 2=RGB. + # param 3/4/5: HLS/RGB values in range 0-100 + # (except 'hue' which is 0..360) + out.write(f'#{idx};2;{random.randrange(101)};{random.randrange(101)};{random.randrange(101)}') + + for row in range(six_rows): + band_count = random.randrange(4, 33) + for band in range(band_count): + # Choose a random color + out.write(f'#{random.randrange(256)}') + + if random.randrange(2): + for col in range(six_width): + out.write(f'{random.choice(sixels)}') + else: + pix_left = six_width + while pix_left > 0: + repeat_count = random.randrange(1, pix_left + 1) + out.write(f'!{repeat_count}{random.choice(sixels)}') + pix_left -= repeat_count + + # Next line + if band + 1 < band_count: + # Move cursor to beginning of current row + out.write('$') + elif row + 1 < six_rows: + # Newline + out.write('-') + + # End sixel + out.write('\033\\') + + # Leave alt screen + out.write('\033[?1049l') + + +if __name__ == '__main__': + main() diff --git a/scripts/generate-builtin-terminfo.py b/scripts/generate-builtin-terminfo.py new file mode 100755 index 0000000..c10373d --- /dev/null +++ b/scripts/generate-builtin-terminfo.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 + +import argparse +import os +import re + + +class Capability: + def __init__(self, name: str, value: bool | int | str): + self._name = name + self._value = value + + @property + def name(self) -> str: + return self._name + + @property + def value(self) -> bool | int | str: + return self._value + + def __lt__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return self._name < other._name + + def __le__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return self._name <= other._name + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return self._name == other._name + + def __ne__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return bool(self._name != other._name) + + def __gt__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return bool(self._name > other._name) + + def __ge__(self, other: object) -> bool: + if not isinstance(other, Capability): + return NotImplemented + return self._name >= other._name + + +class BoolCapability(Capability): + def __init__(self, name: str): + super().__init__(name, True) + + +class IntCapability(Capability): + pass + + +class StringCapability(Capability): + def __init__(self, name: str, value: str): + # see terminfo(5) for valid escape sequences + + # Control characters + def translate_ctrl_chr(m: re.Match[str]) -> str: + ctrl = m.group(1) + if ctrl == '?': + return '\\x7f' + return f'\\x{ord(ctrl) - ord("@"):02x}' + value = re.sub(r'\^([@A-Z[\\\\\]^_?])', translate_ctrl_chr, value) + + # Ensure e.g. \E7 (or \e7) doesn’t get translated to “\0337”, + # which would be interpreted as octal 337 by the C compiler + value = re.sub(r'(\\E|\\e)([0-7])', r'\\033" "\2', value) + + # Replace \E and \e with ESC + value = re.sub(r'\\E|\\e', r'\\033', value) + + # Unescape ,:^ + value = re.sub(r'\\(,|:|\^)', r'\1', value) + + # Replace \s with space + value = value.replace('\\s', ' ') + + # Let \\, \n, \r, \t, \b and \f "fall through", to the C string literal + + if re.search(r'\\l', value): + raise NotImplementedError('\\l escape sequence') + + super().__init__(name, value) + + +class Fragment: + def __init__(self, name: str, description: str): + self._name = name + self._description = description + self._caps = dict[str, Capability]() + + @property + def name(self) -> str: + return self._name + + @property + def description(self) -> str: + return self._description + + @property + def caps(self) -> dict[str, Capability]: + return self._caps + + def add_capability(self, cap: Capability) -> None: + assert cap.name not in self._caps + self._caps[cap.name] = cap + + def del_capability(self, name: str) -> None: + del self._caps[name] + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument('source_entry_name') + parser.add_argument('source', type=argparse.FileType('r')) + parser.add_argument('target_entry_name') + parser.add_argument('target', type=argparse.FileType('w')) + + opts = parser.parse_args() + source_entry_name = opts.source_entry_name + target_entry_name = opts.target_entry_name + source = opts.source + target = opts.target + + lines = list[str]() + for line in source.readlines(): + line = line.strip() + if line.startswith('#'): + continue + lines.append(line) + + fragments = dict[str, Fragment]() + cur_fragment: Fragment | None = None + + for m in re.finditer( + r'(?P(?P[-+\w@]+)\|(?P.+?),)|' + r'(?P(?P\w+),)|' + r'(?P(?P\w+)#(?P(0x)?[0-9a-fA-F]+),)|' + r'(?P(?P\w+)=(?P(.+?)),)', + ''.join(lines)): + + if m.group('name') is not None: + name = m.group('entry_name') + description = m.group('entry_desc') + + assert name not in fragments + fragments[name] = Fragment(name, description) + cur_fragment = fragments[name] + + elif m.group('bool_cap') is not None: + name = m.group('bool_name') + assert cur_fragment is not None + cur_fragment.add_capability(BoolCapability(name)) + + elif m.group('int_cap') is not None: + name = m.group('int_name') + int_value = int(m.group('int_val'), 0) + assert cur_fragment is not None + cur_fragment.add_capability(IntCapability(name, int_value)) + + elif m.group('str_cap') is not None: + name = m.group('str_name') + str_value = m.group('str_val') + assert cur_fragment is not None + cur_fragment.add_capability(StringCapability(name, str_value)) + + else: + assert False + + # Expand ‘use’ capabilities + for frag in fragments.values(): + for cap in frag.caps.values(): + if cap.name == 'use': + assert isinstance(cap, StringCapability) + assert isinstance(cap.value, str) + + use_frag = fragments[cap.value] + for use_cap in use_frag.caps.values(): + frag.add_capability(use_cap) + + + frag.del_capability(cap.name) + break + + entry = fragments[source_entry_name] + + try: + entry.del_capability('RGB') + except KeyError: + pass + + entry.add_capability(IntCapability('Co', 256)) + entry.add_capability(StringCapability('TN', target_entry_name)) + entry.add_capability(StringCapability('name', target_entry_name)) + entry.add_capability(IntCapability('RGB', 8)) # 8 bits per channel + entry.add_capability(StringCapability('query-os-name', os.uname().sysname)) + + terminfo_parts = list[str]() + for cap in sorted(entry.caps.values()): + name = cap.name + value = str(cap.value) + + # Escape ‘“‘ + name = name.replace('"', '\"') + value = value.replace('"', '\"') + + terminfo_parts.append(name) + if isinstance(cap, BoolCapability): + terminfo_parts.append('') + else: + terminfo_parts.append(value) + + terminfo = '\\0" "'.join(terminfo_parts) + + target.write('#pragma once\n') + target.write('\n') + target.write(f'static const char terminfo_capabilities[] = "{terminfo}";') + target.write('\n') + + +if __name__ == '__main__': + main() diff --git a/scripts/generate-emoji-variation-sequences.py b/scripts/generate-emoji-variation-sequences.py new file mode 100644 index 0000000..57e881c --- /dev/null +++ b/scripts/generate-emoji-variation-sequences.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +import argparse + + +class Codepoint: + def __init__(self, start: int, end: None | int = None): + self.start = start + self.end = start if end is None else end + self.vs15 = False + self.vs16 = False + + def __repr__(self) -> str: + return f'{self.start:x}-{self.end:x}, vs15={self.vs15}, vs16={self.vs16}' + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument('input', type=argparse.FileType('r')) + parser.add_argument('output', type=argparse.FileType('w')) + opts = parser.parse_args() + + codepoints: dict[int, Codepoint] = {} + + for line in opts.input: + line = line.rstrip() + if not line: + continue + if line[0] == '#': + continue + + # Example: "0023 FE0E ; text style; # (1.1) NUMBER SIGN" + cps, _ = line.split(';', maxsplit=1) # cps = "0023 FE0F " + cps = cps.strip().split(' ') # cps = ["0023", "FE0F"] + + if len(cps) != 2: + raise NotImplementedError(f'emoji variation sequences with more than one base codepoint: {cps}') + + cp, vs = cps # cp = "0023", vs = "FE0F" + cp = int(cp, 16) # cp = 0x23 + vs = int(vs, 16) # vs = 0xfe0f + + assert vs in [0xfe0e, 0xfe0f] + + if cp not in codepoints: + codepoints[cp] = Codepoint(cp) + + assert codepoints[cp].start == cp + + if vs == 0xfe0e: + codepoints[cp].vs15 = True + else: + codepoints[cp].vs16 = True + + sorted_list = sorted(codepoints.values(), key=lambda cp: cp.start) + + compacted: list[Codepoint] = [] + for i, cp in enumerate(sorted_list): + assert cp.end == cp.start + + if i == 0: + compacted.append(cp) + continue + + last_cp = compacted[-1] + if last_cp.end == cp.start - 1 and last_cp.vs15 == cp.vs15 and last_cp.vs16 == cp.vs16: + compacted[-1].end = cp.start + else: + compacted.append(cp) + + opts.output.write('#pragma once\n') + opts.output.write('#include \n') + opts.output.write('#include \n') + opts.output.write('\n') + opts.output.write('struct emoji_vs {\n') + opts.output.write(' uint32_t start:21;\n') + opts.output.write(' uint32_t end:21;\n') + opts.output.write(' bool vs15:1;\n') + opts.output.write(' bool vs16:1;\n') + opts.output.write('} __attribute__((packed));\n') + opts.output.write('_Static_assert(sizeof(struct emoji_vs) == 6, "unexpected struct size");\n') + opts.output.write('\n') + opts.output.write('#if defined(FOOT_GRAPHEME_CLUSTERING)\n') + opts.output.write('\n') + + opts.output.write(f'static const struct emoji_vs emoji_vs[{len(compacted)}] = {{\n') + + for cp in compacted: + opts.output.write(' {\n') + opts.output.write(f' .start = 0x{cp.start:X},\n') + opts.output.write(f' .end = 0x{cp.end:x},\n') + opts.output.write(f' .vs15 = {"true" if cp.vs15 else "false"},\n') + opts.output.write(f' .vs16 = {"true" if cp.vs16 else "false"},\n') + opts.output.write(' },\n') + + opts.output.write('};\n') + opts.output.write('\n') + opts.output.write('#endif /* FOOT_GRAPHEME_CLUSTERING */\n') + + +if __name__ == '__main__': + main() diff --git a/scripts/srgb.py b/scripts/srgb.py new file mode 100755 index 0000000..a6aa0f4 --- /dev/null +++ b/scripts/srgb.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 + +import argparse +import math + + +# Note: we use a pure gamma 2.2 function, rather than the piece-wise +# sRGB transfer function, since that is what all compositors do. + +def srgb_to_linear(f: float) -> float: + assert(f >= 0 and f <= 1.0) + return math.pow(f, 2.2) + + +def linear_to_srgb(f: float) -> float: + return math.pow(f, 1 / 2.2) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument('c_output', type=argparse.FileType('w')) + parser.add_argument('h_output', type=argparse.FileType('w')) + opts = parser.parse_args() + + linear_table: list[int] = [] + + for i in range(256): + linear_table.append(int(srgb_to_linear(float(i) / 255) * 65535 + 0.5)) + + + opts.h_output.write("#pragma once\n") + opts.h_output.write("#include \n") + opts.h_output.write("\n") + opts.h_output.write('/* 8-bit input, 16-bit output */\n') + opts.h_output.write("extern const uint16_t srgb_decode_8_to_16_table[256];") + + opts.h_output.write('\n') + opts.h_output.write('static inline uint16_t\n') + opts.h_output.write('srgb_decode_8_to_16(uint8_t v)\n') + opts.h_output.write('{\n') + opts.h_output.write(' return srgb_decode_8_to_16_table[v];\n') + opts.h_output.write('}\n') + + opts.h_output.write('\n') + opts.h_output.write('/* 8-bit input, 8-bit output */\n') + opts.h_output.write("extern const uint8_t srgb_decode_8_to_8_table[256];\n") + + opts.h_output.write('\n') + opts.h_output.write('static inline uint8_t\n') + opts.h_output.write('srgb_decode_8_to_8(uint8_t v)\n') + opts.h_output.write('{\n') + opts.h_output.write(' return srgb_decode_8_to_8_table[v];\n') + opts.h_output.write('}\n') + + opts.c_output.write('#include "srgb.h"\n') + opts.c_output.write('\n') + + opts.c_output.write("const uint16_t srgb_decode_8_to_16_table[256] = {\n") + for i in range(256): + opts.c_output.write(f' {linear_table[i]},\n') + opts.c_output.write('};\n') + + opts.c_output.write("const uint8_t srgb_decode_8_to_8_table[256] = {\n") + for i in range(256): + opts.c_output.write(f' {linear_table[i] >> 8},\n') + opts.c_output.write('};\n') + + +if __name__ == '__main__': + main() diff --git a/search.c b/search.c new file mode 100644 index 0000000..0fa2756 --- /dev/null +++ b/search.c @@ -0,0 +1,2077 @@ +#include "search.h" + +#include +#include + +#include +#include + +#define LOG_MODULE "search" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "char32.h" +#include "commands.h" +#include "config.h" +#include "extract.h" +#include "grid.h" +#include "input.h" +#include "key-binding.h" +#include "misc.h" +#include "quirks.h" +#include "render.h" +#include "selection.h" +#include "shm.h" +#include "unicode-mode.h" +#include "util.h" +#include "xmalloc.h" + +/* Hard cap on counting matches, to keep the counter cheap on huge + * scrollbacks with very generic queries. */ +#define SEARCH_COUNT_CAP 9999 + +/* + * Ensures a "new" viewport doesn't contain any unallocated rows. + * + * This is done by first checking if the *first* row is NULL. If so, + * we move the viewport *forward*, until the first row is non-NULL. At + * this point, the entire viewport should be allocated rows only. + * + * If the first row already was non-NULL, we instead check the *last* + * row, and if it is NULL, we move the viewport *backward* until the + * last row is non-NULL. + */ +static int +ensure_view_is_allocated(struct terminal *term, int new_view) +{ + struct grid *grid = term->grid; + int view_end = (new_view + term->rows - 1) & (grid->num_rows - 1); + + if (grid->rows[new_view] == NULL) { + while (grid->rows[new_view] == NULL) + new_view = (new_view + 1) & (grid->num_rows - 1); + } + + else if (grid->rows[view_end] == NULL) { + while (grid->rows[view_end] == NULL) { + new_view--; + if (new_view < 0) + new_view += grid->num_rows; + view_end = (new_view + term->rows - 1) & (grid->num_rows - 1); + } + } + +#if defined(_DEBUG) + for (size_t r = 0; r < term->rows; r++) + xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL); +#endif + + return new_view; +} + +static bool +search_ensure_size(struct terminal *term, size_t wanted_size) +{ + while (wanted_size >= term->search.sz) { + size_t new_sz = term->search.sz == 0 ? 64 : term->search.sz * 2; + char32_t *new_buf = realloc(term->search.buf, new_sz * sizeof(term->search.buf[0])); + + if (new_buf == NULL) { + LOG_ERRNO("failed to resize search buffer"); + return false; + } + + term->search.buf = new_buf; + term->search.sz = new_sz; + } + + return true; +} + +static bool +has_wrapped_around_left(const struct terminal *term, int abs_row_no) +{ + int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no); + return rebased_row == term->grid->num_rows - 1 || term->grid->rows[abs_row_no] == NULL; +} + +static bool +has_wrapped_around_right(const struct terminal *term, int abs_row_no) +{ + int rebased_row = grid_row_abs_to_sb(term->grid, term->rows, abs_row_no); + return rebased_row == 0; +} + +static void +search_history_push(struct terminal *term, const char32_t *buf, size_t len) +{ + if (len == 0) + return; + + /* Avoid pushing a duplicate of the most recent entry */ + if (term->search.history_tail != NULL && + term->search.history_tail->len == len && + c32ncmp(term->search.history_tail->buf, buf, len) == 0) + return; + + struct search_history_entry *e = xcalloc(1, sizeof(*e)); + e->buf = xmalloc((len + 1) * sizeof(char32_t)); + memcpy(e->buf, buf, len * sizeof(char32_t)); + e->buf[len] = U'\0'; + e->len = len; + + e->prev = term->search.history_tail; + if (term->search.history_tail != NULL) + term->search.history_tail->next = e; + else + term->search.history_head = e; + term->search.history_tail = e; +} + +static void +search_cancel_keep_selection(struct terminal *term) +{ + struct wl_window *win = term->window; + wayl_win_subsurface_destroy(&win->search); + + if (term->search.len > 0) { + /* Save into history */ + search_history_push(term, term->search.buf, term->search.len); + + free(term->search.last.buf); + term->search.last.buf = term->search.buf; + term->search.last.len = term->search.len; + } else + free(term->search.buf); + + /* Free compiled regex */ + if (term->search.regex_compiled != NULL) { + regfree(term->search.regex_compiled); + free(term->search.regex_compiled); + term->search.regex_compiled = NULL; + } + term->search.regex_valid = false; + + term->search.buf = NULL; + term->search.len = term->search.sz = 0; + + term->search.cursor = 0; + term->search.match = (struct coord){-1, -1}; + term->search.match_len = 0; + term->search.total_count = 0; + term->search.current_idx = 0; + term->search.wrapped = false; + term->search.history_pos = NULL; + term->is_searching = false; + term->render.search_glyph_offset = 0; + + /* Reset IME state */ + if (term_ime_is_enabled(term)) { + term_ime_disable(term); + term_ime_enable(term); + } + + term_xcursor_update(term); + render_refresh(term); +} + +void +search_begin(struct terminal *term) +{ + LOG_DBG("search: begin"); + + search_cancel_keep_selection(term); + selection_cancel(term); + + /* Reset IME state */ + if (term_ime_is_enabled(term)) { + term_ime_disable(term); + term_ime_enable(term); + } + + /* On-demand instantiate wayland surface */ + bool ret = wayl_win_subsurface_new( + term->window, &term->window->search, false); + xassert(ret); + + const struct grid *grid = term->grid; + term->search.original_view = grid->view; + term->search.view_followed_offset = grid->view == grid->offset; + term->is_searching = true; + + term->search.len = 0; + term->search.sz = 64; + term->search.buf = xmalloc(term->search.sz * sizeof(term->search.buf[0])); + term->search.buf[0] = U'\0'; + + term_xcursor_update(term); + render_refresh_search(term); +} + +void +search_cancel(struct terminal *term) +{ + if (!term->is_searching) + return; + + search_cancel_keep_selection(term); + selection_cancel(term); +} + +void +search_selection_cancelled(struct terminal *term) +{ + term->search.match = (struct coord){-1, -1}; + term->search.match_len = 0; + render_refresh_search(term); +} + +static void +search_update_selection(struct terminal *term, const struct range *match) +{ + struct grid *grid = term->grid; + int start_row = match->start.row; + int start_col = match->start.col; + int end_row = match->end.row; + int end_col = match->end.col; + + xassert(start_row >= 0); + xassert(start_row < grid->num_rows); + + bool move_viewport = true; + + int view_end = (grid->view + term->rows - 1) & (grid->num_rows - 1); + if (view_end >= grid->view) { + /* Viewport does *not* wrap around */ + if (start_row >= grid->view && end_row <= view_end) + move_viewport = false; + } else { + /* Viewport wraps */ + if (start_row >= grid->view || end_row <= view_end) + move_viewport = false; + } + + if (move_viewport) { + int rebased_new_view = grid_row_abs_to_sb(grid, term->rows, start_row); + + rebased_new_view -= term->rows / 2; + rebased_new_view = + min(max(rebased_new_view, 0), grid->num_rows - term->rows); + + const int old_view = grid->view; + int new_view = grid_row_sb_to_abs(grid, term->rows, rebased_new_view); + + /* Scrollback may not be completely filled yet */ + { + const int mask = grid->num_rows - 1; + while (grid->rows[new_view] == NULL) + new_view = (new_view + 1) & mask; + } + +#if defined(_DEBUG) + /* Verify all to-be-visible rows have been allocated */ + for (int r = 0; r < term->rows; r++) + xassert(grid->rows[(new_view + r) & (grid->num_rows - 1)] != NULL); +#endif + +#if defined(_DEBUG) + { + int rel_start_row = grid_row_abs_to_sb(grid, term->rows, start_row); + int rel_view = grid_row_abs_to_sb(grid, term->rows, new_view); + xassert(rel_view <= rel_start_row); + xassert(rel_start_row < rel_view + term->rows); + } +#endif + + /* Update view */ + grid->view = new_view; + if (new_view != old_view) + term_damage_view(term); + } + + if (start_row != term->search.match.row || + start_col != term->search.match.col || + + /* Pointer leave events trigger selection_finalize() :/ */ + !term->selection.ongoing) + { + int selection_row = start_row - grid->view + grid->num_rows; + selection_row &= grid->num_rows - 1; + + selection_start( + term, start_col, selection_row, SELECTION_CHAR_WISE, false); + + term->search.match.row = start_row; + term->search.match.col = start_col; + } + + /* Update selection endpoint */ + { + int selection_row = end_row - grid->view + grid->num_rows; + selection_row &= grid->num_rows - 1; + selection_update(term, end_col, selection_row); + } +} + +static bool +search_is_case_sensitive(const struct terminal *term) +{ + switch (term->search.case_mode) { + case SEARCH_CASE_SENSITIVE: return true; + case SEARCH_CASE_INSENSITIVE: return false; + case SEARCH_CASE_SMART: return hasc32upper(term->search.buf); + } + return true; +} + +static bool +search_cell_is_word(const struct terminal *term, const struct cell *cell) +{ + char32_t base = cell->wc; + if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) { + const struct composed *c = + composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); + base = c->chars[0]; + } + if (base == 0 || base >= CELL_SPACER) + return false; + return isword(base, false, term->conf->word_delimiters); +} + +/* True if the cell *immediately before* (col-1, row), wrapping back a + * row if needed, is a word character. Returns false at scrollback + * boundaries (treat boundary as non-word). */ +static bool +search_neighbor_is_word(const struct terminal *term, int row, int col, + bool look_right) +{ + const struct grid *grid = term->grid; + int r = row, c = col; + + if (look_right) { + c++; + if (c >= term->cols) { + r = (r + 1) & (grid->num_rows - 1); + c = 0; + if (has_wrapped_around_right(term, r)) + return false; + } + } else { + c--; + if (c < 0) { + r = (r - 1 + grid->num_rows) & (grid->num_rows - 1); + c = term->cols - 1; + if (has_wrapped_around_left(term, r)) + return false; + } + } + + const struct row *gr = grid->rows[r]; + if (gr == NULL) + return false; + return search_cell_is_word(term, &gr->cells[c]); +} + +/* Extract a row's printable cells into a UTF-8 buffer plus a + * byte-offset → cell-column map. The map has one entry per UTF-8 + * code unit (so byte_to_col[i] is the column the byte at offset i + * came from). The trailing NUL has the column == term->cols. */ +struct row_text { + char *utf8; + size_t len; + int *byte_to_col; +}; + +static bool +extract_row_text(const struct terminal *term, const struct row *row, + struct row_text *out) +{ + out->utf8 = NULL; + out->len = 0; + out->byte_to_col = NULL; + + if (row == NULL) + return false; + + /* Worst case: every cell becomes 4 UTF-8 bytes for the base char, + * plus combining chars (cap to a few). */ + size_t cap = (size_t)term->cols * 8 + 1; + char *buf = xmalloc(cap); + int *map = xmalloc(cap * sizeof(int)); + size_t pos = 0; + + mbstate_t ps = {0}; + + for (int col = 0; col < term->cols; col++) { + const struct cell *cell = &row->cells[col]; + char32_t base = cell->wc; + + if (base >= CELL_SPACER) + continue; /* right-half of wide char */ + + if (base == 0) + base = U' '; + + const struct composed *composed = NULL; + if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) { + composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); + base = composed->chars[0]; + } + + size_t encoded = c32rtomb(&buf[pos], base, &ps); + if (encoded == (size_t)-1) { + buf[pos] = '?'; + encoded = 1; + memset(&ps, 0, sizeof(ps)); + } + for (size_t i = 0; i < encoded; i++) + map[pos + i] = col; + pos += encoded; + + if (composed != NULL) { + for (size_t j = 1; j < composed->count; j++) { + size_t e = c32rtomb(&buf[pos], composed->chars[j], &ps); + if (e == (size_t)-1) { + buf[pos] = '?'; + e = 1; + memset(&ps, 0, sizeof(ps)); + } + for (size_t i = 0; i < e; i++) + map[pos + i] = col; + pos += e; + } + } + } + + buf[pos] = '\0'; + map[pos] = term->cols; + + out->utf8 = buf; + out->len = pos; + out->byte_to_col = map; + return true; +} + +static void +free_row_text(struct row_text *rt) +{ + free(rt->utf8); + free(rt->byte_to_col); + rt->utf8 = NULL; + rt->byte_to_col = NULL; + rt->len = 0; +} + +static bool +search_compile_regex(struct terminal *term) +{ + /* Free any previously compiled regex */ + if (term->search.regex_compiled != NULL) { + regfree(term->search.regex_compiled); + free(term->search.regex_compiled); + term->search.regex_compiled = NULL; + } + term->search.regex_valid = false; + + if (!term->search.regex || term->search.len == 0) + return false; + + /* Convert search buffer to UTF-8 */ + char pattern[term->search.len * 4 + 1]; + mbstate_t ps = {0}; + size_t out = 0; + for (size_t i = 0; i < term->search.len; i++) { + size_t e = c32rtomb(&pattern[out], term->search.buf[i], &ps); + if (e == (size_t)-1) { + pattern[out++] = '?'; + memset(&ps, 0, sizeof(ps)); + } else { + out += e; + } + } + pattern[out] = '\0'; + + regex_t *re = xmalloc(sizeof(*re)); + int flags = REG_EXTENDED; + if (!search_is_case_sensitive(term)) + flags |= REG_ICASE; + + if (regcomp(re, pattern, flags) != 0) { + free(re); + return false; + } + + term->search.regex_compiled = re; + term->search.regex_valid = true; + return true; +} + +/* Find one regex match on a single row. Returns true if found and + * fills [start_col, end_col]. */ +static bool +regex_find_in_row(const struct terminal *term, const struct row *row, + int min_col, int max_col, int *out_start, int *out_end) +{ + if (!term->search.regex_valid || row == NULL) + return false; + + struct row_text rt; + if (!extract_row_text(term, row, &rt)) + return false; + + bool found = false; + regmatch_t m; + size_t scan_from = 0; + + /* Find matches; honor min_col by skipping ones that end before it, + * and max_col by stopping after them. Pick the *first* match whose + * starting column is in [min_col, max_col]. */ + while (scan_from <= rt.len) { + if (regexec(term->search.regex_compiled, rt.utf8 + scan_from, 1, &m, + scan_from > 0 ? REG_NOTBOL : 0) != 0) + break; + if (m.rm_so == m.rm_eo) { + /* Zero-width — advance one byte to avoid infinite loop */ + scan_from++; + continue; + } + + int s_col = rt.byte_to_col[scan_from + m.rm_so]; + int e_col = rt.byte_to_col[scan_from + m.rm_eo - 1]; + + if (s_col >= min_col && s_col <= max_col) { + if (term->search.whole_word) { + if ((s_col > 0 && search_cell_is_word(term, &row->cells[s_col - 1])) || + (e_col + 1 < term->cols && search_cell_is_word(term, &row->cells[e_col + 1]))) + { + scan_from += m.rm_eo; + continue; + } + } + *out_start = s_col; + *out_end = e_col; + found = true; + break; + } + + scan_from += m.rm_eo; + } + + free_row_text(&rt); + return found; +} + +/* Walk rows in the requested direction, returning the first regex + * match found within the [start, end] range. */ +static bool +regex_find_next(const struct terminal *term, enum search_direction direction, + struct coord abs_start, struct coord abs_end, + struct range *match) +{ + const struct grid *grid = term->grid; + const bool backward = direction != SEARCH_FORWARD; + + int row_no = abs_start.row; + int from_col = abs_start.col; + int to_col = (row_no == abs_end.row) ? abs_end.col : + (backward ? 0 : term->cols - 1); + + while (true) { + const struct row *row = grid->rows[row_no]; + if (row != NULL) { + int min_col = backward ? min(from_col, to_col) : from_col; + int max_col = backward ? max(from_col, to_col) : to_col; + + int s, e; + if (regex_find_in_row(term, row, min_col, max_col, &s, &e)) { + match->start = (struct coord){s, row_no}; + match->end = (struct coord){e, row_no}; + return true; + } + } + + if (row_no == abs_end.row) + break; + + if (backward) { + row_no = (row_no - 1 + grid->num_rows) & (grid->num_rows - 1); + from_col = term->cols - 1; + to_col = (row_no == abs_end.row) ? abs_end.col : 0; + } else { + row_no = (row_no + 1) & (grid->num_rows - 1); + from_col = 0; + to_col = (row_no == abs_end.row) ? abs_end.col : term->cols - 1; + } + } + + return false; +} + +static ssize_t +matches_cell(const struct terminal *term, const struct cell *cell, size_t search_ofs) +{ + assert(search_ofs < term->search.len); + + char32_t base = cell->wc; + const struct composed *composed = NULL; + + if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) + { + composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); + base = composed->chars[0]; + } + + if (composed == NULL && base == 0 && term->search.buf[search_ofs] == U' ') + return 1; + + if (search_is_case_sensitive(term)) { + if (c32ncmp(&base, &term->search.buf[search_ofs], 1) != 0) + return -1; + } else { + if (c32ncasecmp(&base, &term->search.buf[search_ofs], 1) != 0) + return -1; + } + + if (composed != NULL) { + if (search_ofs + composed->count > term->search.len) + return -1; + + for (size_t j = 1; j < composed->count; j++) { + if (composed->chars[j] != term->search.buf[search_ofs + j]) + return -1; + } + } + + return composed != NULL ? composed->count : 1; +} + +static bool +find_next(struct terminal *term, enum search_direction direction, + struct coord abs_start, struct coord abs_end, struct range *match) +{ +#define ROW_DEC(_r) ((_r) = ((_r) - 1 + grid->num_rows) & (grid->num_rows - 1)) +#define ROW_INC(_r) ((_r) = ((_r) + 1) & (grid->num_rows - 1)) + + if (term->search.regex && term->search.regex_valid) + return regex_find_next(term, direction, abs_start, abs_end, match); + + struct grid *grid = term->grid; + const bool backward = direction != SEARCH_FORWARD; + + LOG_DBG("%s: start: %dx%d, end: %dx%d", backward ? "backward" : "forward", + abs_start.row, abs_start.col, abs_end.row, abs_end.col); + + xassert(abs_start.row >= 0); + xassert(abs_start.row < grid->num_rows); + xassert(abs_start.col >= 0); + xassert(abs_start.col < term->cols); + + xassert(abs_end.row >= 0); + xassert(abs_end.row < grid->num_rows); + xassert(abs_end.col >= 0); + xassert(abs_end.col < term->cols); + + for (int match_start_row = abs_start.row, match_start_col = abs_start.col; + ; + backward ? ROW_DEC(match_start_row) : ROW_INC(match_start_row)) { + + const struct row *row = grid->rows[match_start_row]; + if (row == NULL) { + if (match_start_row == abs_end.row) + break; + continue; + } + + for (; + backward ? match_start_col >= 0 : match_start_col < term->cols; + backward ? match_start_col-- : match_start_col++) + { + if (matches_cell(term, &row->cells[match_start_col], 0) < 0) { + if (match_start_row == abs_end.row && + match_start_col == abs_end.col) + { + break; + } + continue; + } + + /* + * Got a match on the first letter. Now we'll see if the + * rest of the search buffer matches. + */ + + LOG_DBG("search: initial match at row=%d, col=%d", + match_start_row, match_start_col); + + int match_end_row = match_start_row; + int match_end_col = match_start_col; + const struct row *match_row = row; + size_t match_len = 0; + + for (size_t i = 0; i < term->search.len;) { + if (match_end_col >= term->cols) { + ROW_INC(match_end_row); + match_end_col = 0; + + match_row = grid->rows[match_end_row]; + if (match_row == NULL) + break; + } + + if (match_row->cells[match_end_col].wc >= CELL_SPACER) { + match_end_col++; + continue; + } + + ssize_t additional_chars = matches_cell( + term, &match_row->cells[match_end_col], i); + if (additional_chars < 0) + break; + + i += additional_chars; + match_len += additional_chars; + match_end_col++; + + while (match_end_col < term->cols && + match_row->cells[match_end_col].wc > CELL_SPACER) + { + match_end_col++; + } + } + + if (match_len != term->search.len) { + /* Didn't match (completely) */ + + if (match_start_row == abs_end.row && + match_start_col == abs_end.col) + { + break; + } + + continue; + } + + if (term->search.whole_word) { + /* Reject if neighbour cells are word-chars */ + if (search_neighbor_is_word( + term, match_start_row, match_start_col, false) || + search_neighbor_is_word( + term, match_end_row, match_end_col - 1, true)) + { + if (match_start_row == abs_end.row && + match_start_col == abs_end.col) + { + break; + } + continue; + } + } + + *match = (struct range){ + .start = {match_start_col, match_start_row}, + .end = {match_end_col - 1, match_end_row}, + }; + + return true; + } + + if (match_start_row == abs_end.row && match_start_col == abs_end.col) + break; + + match_start_col = backward ? term->cols - 1 : 0; + } + + return false; +} + +/* Count total matches across the whole grid, capped at + * SEARCH_COUNT_CAP. Returns the cap if we hit it. */ +static size_t +search_count_all(struct terminal *term) +{ + if (term->search.len == 0) + return 0; + if (term->search.regex && !term->search.regex_valid) + return 0; + + struct grid *grid = term->grid; + + /* Start at the very top of the scrollback (oldest row) */ + int oldest = (grid->offset + term->rows) & (grid->num_rows - 1); + + struct coord pos = { 0, oldest }; + struct coord end = { term->cols - 1, oldest }; + /* end.row should be the row *before* oldest, i.e. the newest */ + end.row = (oldest - 1 + grid->num_rows) & (grid->num_rows - 1); + + size_t count = 0; + while (count < SEARCH_COUNT_CAP) { + struct range m; + if (!find_next(term, SEARCH_FORWARD, pos, end, &m)) + break; + count++; + + /* Advance one cell past the match start */ + pos.col = m.start.col + 1; + pos.row = m.start.row; + if (pos.col >= term->cols) { + pos.col = 0; + pos.row = (pos.row + 1) & (grid->num_rows - 1); + if (pos.row == oldest) + break; /* wrapped */ + } + + if (pos.row == end.row && pos.col > end.col) + break; + } + + return count; +} + +/* Recompute current_idx by counting matches from the top of the + * scrollback up to (and including) the current match position. */ +static size_t +search_compute_current_idx(struct terminal *term) +{ + if (term->search.match_len == 0 || term->search.len == 0) + return 0; + if (term->search.regex && !term->search.regex_valid) + return 0; + + struct grid *grid = term->grid; + int oldest = (grid->offset + term->rows) & (grid->num_rows - 1); + + /* Sentinel "newest cell" - we'll abort when we pass our own match */ + struct coord newest = { term->cols - 1, + (oldest - 1 + grid->num_rows) & (grid->num_rows - 1) }; + + struct coord pos = { 0, oldest }; + + size_t idx = 0; + while (idx < SEARCH_COUNT_CAP) { + struct range m; + if (!find_next(term, SEARCH_FORWARD, pos, newest, &m)) + break; + idx++; + + if (m.start.row == term->search.match.row && + m.start.col == term->search.match.col) + return idx; + + pos.col = m.start.col + 1; + pos.row = m.start.row; + if (pos.col >= term->cols) { + pos.col = 0; + pos.row = (pos.row + 1) & (grid->num_rows - 1); + if (pos.row == oldest) + break; + } + } + + return 0; +} + +static void +search_find_next(struct terminal *term, enum search_direction direction) +{ + struct grid *grid = term->grid; + + /* Recompile regex if active (cheap when len==0; no-op otherwise) */ + if (term->search.regex) + search_compile_regex(term); + + if (term->search.len == 0) { + term->search.match = (struct coord){-1, -1}; + term->search.match_len = 0; + term->search.total_count = 0; + term->search.current_idx = 0; + term->search.wrapped = false; + selection_cancel(term); + return; + } + + struct coord start = term->search.match; + size_t len = term->search.match_len; + + xassert((len == 0 && start.row == -1 && start.col == -1) || + (len > 0 && start.row >= 0 && start.col >= 0)); + + if (len == 0) { + /* No previous match, start from the top, or bottom, of the scrollback */ + switch (direction) { + case SEARCH_FORWARD: + start.row = grid_row_absolute_in_view(grid, 0); + start.col = 0; + break; + + case SEARCH_BACKWARD: + case SEARCH_BACKWARD_SAME_POSITION: + start.row = grid_row_absolute_in_view(grid, term->rows - 1); + start.col = term->cols - 1; + break; + } + } else { + /* Continue from last match */ + xassert(start.row >= 0); + xassert(start.col >= 0); + + switch (direction) { + case SEARCH_BACKWARD_SAME_POSITION: + break; + + case SEARCH_BACKWARD: + if (--start.col < 0) { + start.col = term->cols - 1; + start.row += grid->num_rows - 1; + start.row &= grid->num_rows - 1; + } + break; + + case SEARCH_FORWARD: + if (++start.col >= term->cols) { + start.col = 0; + start.row++; + start.row &= grid->num_rows - 1; + } + break; + } + + xassert(start.row >= 0); + xassert(start.row < grid->num_rows); + xassert(start.col >= 0); + xassert(start.col < term->cols); + } + + LOG_DBG( + "update: %s: starting at row=%d col=%d " + "(offset = %d, view = %d)", + direction != SEARCH_FORWARD ? "backward" : "forward", + start.row, start.col, + grid->offset, grid->view); + + struct coord end = start; + switch (direction) { + case SEARCH_FORWARD: + /* Search forward, until we reach the cell *before* current start */ + if (--end.col < 0) { + end.col = term->cols - 1; + end.row += grid->num_rows - 1; + end.row &= grid->num_rows - 1; + } + break; + + case SEARCH_BACKWARD: + case SEARCH_BACKWARD_SAME_POSITION: + /* Search backwards, until we reach the cell *after* current start */ + if (++end.col >= term->cols) { + end.col = 0; + end.row++; + end.row &= grid->num_rows - 1; + } + break; + } + + /* Remember previous match position for wrap detection */ + const struct coord prev_match = term->search.match; + const size_t prev_match_len = term->search.match_len; + + struct range match; + bool found = find_next(term, direction, start, end, &match); + + term->search.wrapped = false; + + if (found) { + LOG_DBG("primary match found at %dx%d", + match.start.row, match.start.col); + + /* Detect wrap: if we had a prior match and the new match is in + * the "wrong" direction relative to it, we wrapped. */ + if (prev_match_len > 0 && + direction != SEARCH_BACKWARD_SAME_POSITION) + { + int oldest = (grid->offset + term->rows) & (grid->num_rows - 1); + int prev_rebased = (prev_match.row - oldest + grid->num_rows) + & (grid->num_rows - 1); + int new_rebased = (match.start.row - oldest + grid->num_rows) + & (grid->num_rows - 1); + + if (direction == SEARCH_FORWARD) { + if (new_rebased < prev_rebased || + (new_rebased == prev_rebased && + match.start.col <= prev_match.col)) + term->search.wrapped = true; + } else { + if (new_rebased > prev_rebased || + (new_rebased == prev_rebased && + match.start.col >= prev_match.col)) + term->search.wrapped = true; + } + } + + search_update_selection(term, &match); + term->search.match = match.start; + term->search.match_len = term->search.len; + } else { + LOG_DBG("no match"); + term->search.match = (struct coord){-1, -1}; + term->search.match_len = 0; + selection_cancel(term); + } + + /* Refresh counter */ + term->search.total_count = search_count_all(term); + term->search.current_idx = search_compute_current_idx(term); +#undef ROW_DEC +} + +struct search_match_iterator +search_matches_new_iter(struct terminal *term) +{ + return (struct search_match_iterator){ + .term = term, + .start = {0, 0}, + }; +} + +struct range +search_matches_next(struct search_match_iterator *iter) +{ + struct terminal *term = iter->term; + struct grid *grid = term->grid; + + if (term->search.match_len == 0) + goto no_match; + + if (iter->start.row >= term->rows) + goto no_match; + + xassert(iter->start.row >= 0); + xassert(iter->start.row < term->rows); + xassert(iter->start.col >= 0); + xassert(iter->start.col < term->cols); + + struct coord abs_start = iter->start; + abs_start.row = grid_row_absolute_in_view(grid, abs_start.row); + + struct coord abs_end = { + term->cols - 1, + grid_row_absolute_in_view(grid, term->rows - 1)}; + + /* BUG: matches *starting* outside the view, but ending *inside*, aren't matched */ + struct range match; + bool found = find_next(term, SEARCH_FORWARD, abs_start, abs_end, &match); + if (!found) + goto no_match; + + LOG_DBG("match at (absolute coordinates) %dx%d-%dx%d", + match.start.row, match.start.col, + match.end.row, match.end.col); + + /* Convert absolute row numbers back to view relative */ + match.start.row = match.start.row - grid->view + grid->num_rows; + match.start.row &= grid->num_rows - 1; + match.end.row = match.end.row - grid->view + grid->num_rows; + match.end.row &= grid->num_rows - 1; + + LOG_DBG("match at (view-local coordinates) %dx%d-%dx%d, view=%d", + match.start.row, match.start.col, + match.end.row, match.end.col, grid->view); + + /* Assert match end comes *after* the match start */ + xassert(match.end.row > match.start.row || + (match.end.row == match.start.row && + match.end.col >= match.start.col)); + + /* Assert the match starts at, or after, the iterator position */ + xassert(match.start.row > iter->start.row || + (match.start.row == iter->start.row && + match.start.col >= iter->start.col)); + + /* Continue at next column, next time */ + iter->start.row = match.start.row; + iter->start.col = match.start.col + 1; + + if (iter->start.col >= term->cols) { + iter->start.col = 0; + iter->start.row++; /* Overflow is caught in next iteration */ + } + + xassert(iter->start.row >= 0); + xassert(iter->start.row <= term->rows); + xassert(iter->start.col >= 0); + xassert(iter->start.col < term->cols); + return match; + +no_match: + iter->start.row = -1; + iter->start.col = -1; + return (struct range){{-1, -1}, {-1, -1}}; +} + +static void +add_wchars(struct terminal *term, char32_t *src, size_t count) +{ + /* Strip non-printable characters */ + for (size_t i = 0, j = 0, orig_count = count; i < orig_count; i++) { + if (isc32print(src[i])) + src[j++] = src[i]; + else + count--; + } + + if (!search_ensure_size(term, term->search.len + count)) + return; + + xassert(term->search.len + count < term->search.sz); + + memmove(&term->search.buf[term->search.cursor + count], + &term->search.buf[term->search.cursor], + (term->search.len - term->search.cursor) * sizeof(char32_t)); + + memcpy(&term->search.buf[term->search.cursor], src, count * sizeof(char32_t)); + + term->search.len += count; + term->search.cursor += count; + term->search.buf[term->search.len] = U'\0'; +} + +void +search_add_chars(struct terminal *term, const char *src, size_t count) +{ + size_t chars = mbsntoc32(NULL, src, count, 0); + if (chars == (size_t)-1) { + LOG_ERRNO("failed to convert %.*s to Unicode", (int)count, src); + return; + } + + char32_t c32s[chars + 1]; + mbsntoc32(c32s, src, count, chars); + add_wchars(term, c32s, chars); +} + +enum extend_direction {SEARCH_EXTEND_LEFT, SEARCH_EXTEND_RIGHT}; + +static bool +coord_advance_left(const struct terminal *term, struct coord *pos, + const struct row **row) +{ + const struct grid *grid = term->grid; + struct coord new_pos = *pos; + + if (--new_pos.col < 0) { + new_pos.row = (new_pos.row - 1 + grid->num_rows) & (grid->num_rows - 1); + new_pos.col = term->cols - 1; + + if (has_wrapped_around_left(term, new_pos.row)) + return false; + + if (row != NULL) + *row = grid->rows[new_pos.row]; + } + + *pos = new_pos; + return true; +} + +static bool +coord_advance_right(const struct terminal *term, struct coord *pos, + const struct row **row) +{ + const struct grid *grid = term->grid; + struct coord new_pos = *pos; + + if (++new_pos.col >= term->cols) { + new_pos.row = (new_pos.row + 1) & (grid->num_rows - 1); + new_pos.col = 0; + + if (has_wrapped_around_right(term, new_pos.row)) + return false; + + if (row != NULL) + *row = grid->rows[new_pos.row]; + } + + *pos = new_pos; + return true; +} + +static bool +search_extend_find_char(const struct terminal *term, struct coord *target, + enum extend_direction direction) +{ + if (term->search.match_len == 0) + return false; + + struct coord pos = direction == SEARCH_EXTEND_LEFT + ? selection_get_start(term) : selection_get_end(term); + xassert(pos.row >= 0); + xassert(pos.row < term->grid->num_rows); + + *target = pos; + + const struct row *row = term->grid->rows[pos.row]; + + while (true) { + switch (direction) { + case SEARCH_EXTEND_LEFT: + if (!coord_advance_left(term, &pos, &row)) + return false; + break; + + case SEARCH_EXTEND_RIGHT: + if (!coord_advance_right(term, &pos, &row)) + return false; + break; + } + + const char32_t wc = row->cells[pos.col].wc; + + if (wc >= CELL_SPACER || wc == U'\0') + continue; + + *target = pos; + return true; + } +} + +static bool +search_extend_find_char_left(const struct terminal *term, struct coord *target) +{ + return search_extend_find_char(term, target, SEARCH_EXTEND_LEFT); +} + +static bool +search_extend_find_char_right(const struct terminal *term, struct coord *target) +{ + return search_extend_find_char(term, target, SEARCH_EXTEND_RIGHT); +} + +static bool +search_extend_find_word(const struct terminal *term, bool spaces_only, + struct coord *target, enum extend_direction direction) +{ + if (term->search.match_len == 0) + return false; + + struct grid *grid = term->grid; + struct coord pos = direction == SEARCH_EXTEND_LEFT + ? selection_get_start(term) + : selection_get_end(term); + + xassert(pos.row >= 0); + xassert(pos.row < grid->num_rows); + + *target = pos; + + /* First character to consider is the *next* character */ + switch (direction) { + case SEARCH_EXTEND_LEFT: + if (!coord_advance_left(term, &pos, NULL)) + return false; + break; + + case SEARCH_EXTEND_RIGHT: + if (!coord_advance_right(term, &pos, NULL)) + return false; + break; + } + + xassert(pos.row >= 0); + xassert(pos.row < grid->num_rows); + xassert(grid->rows[pos.row] != NULL); + + /* Find next word boundary */ + switch (direction) { + case SEARCH_EXTEND_LEFT: + selection_find_word_boundary_left(term, &pos, spaces_only); + break; + + case SEARCH_EXTEND_RIGHT: + selection_find_word_boundary_right(term, &pos, spaces_only, false); + break; + } + + *target = pos; + return true; +} + +static bool +search_extend_find_word_left(const struct terminal *term, bool spaces_only, + struct coord *target) +{ + return search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_LEFT); +} + +static bool +search_extend_find_word_right(const struct terminal *term, bool spaces_only, + struct coord *target) +{ + return search_extend_find_word(term, spaces_only, target, SEARCH_EXTEND_RIGHT); +} + +static bool +search_extend_find_line(const struct terminal *term, struct coord *target, + enum extend_direction direction) +{ + if (term->search.match_len == 0) + return false; + + struct coord pos = direction == SEARCH_EXTEND_LEFT + ? selection_get_start(term) : selection_get_end(term); + + xassert(pos.row >= 0); + xassert(pos.row < term->grid->num_rows); + + *target = pos; + + const struct grid *grid = term->grid; + + switch (direction) { + case SEARCH_EXTEND_LEFT: + pos.row = (pos.row - 1 + grid->num_rows) & (grid->num_rows - 1); + if (has_wrapped_around_left(term, pos.row)) + return false; + break; + + case SEARCH_EXTEND_RIGHT: + pos.row = (pos.row + 1) & (grid->num_rows - 1); + if (has_wrapped_around_right(term, pos.row)) + return false; + break; + } + + *target = pos; + return true; +} + +static bool +search_extend_find_line_up(const struct terminal *term, struct coord *target) +{ + return search_extend_find_line(term, target, SEARCH_EXTEND_LEFT); +} + +static bool +search_extend_find_line_down(const struct terminal *term, struct coord *target) +{ + return search_extend_find_line(term, target, SEARCH_EXTEND_RIGHT); +} + +static void +search_extend_left(struct terminal *term, const struct coord *target) +{ + if (term->search.match_len == 0) + return; + + const struct coord last_coord = selection_get_start(term); + struct coord pos = *target; + const struct row *row = term->grid->rows[pos.row]; + + const bool move_cursor = term->search.cursor != 0; + + struct extraction_context *ctx = extract_begin(SELECTION_NONE, false); + if (ctx == NULL) + return; + + while (pos.col != last_coord.col || pos.row != last_coord.row) { + if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx)) + break; + if (!coord_advance_right(term, &pos, &row)) + break; + } + + char32_t *new_text; + size_t new_len; + + if (!extract_finish_wide(ctx, &new_text, &new_len)) + return; + + if (!search_ensure_size(term, term->search.len + new_len)) + return; + + memmove(&term->search.buf[new_len], &term->search.buf[0], + term->search.len * sizeof(term->search.buf[0])); + + size_t actually_copied = 0; + for (size_t i = 0; i < new_len; i++) { + if (new_text[i] == U'\n') { + /* extract() adds newlines, which we never match against */ + continue; + } + + term->search.buf[actually_copied++] = new_text[i]; + term->search.len++; + } + + xassert(actually_copied <= new_len); + if (actually_copied < new_len) { + memmove( + &term->search.buf[actually_copied], &term->search.buf[new_len], + (term->search.len - actually_copied) * sizeof(term->search.buf[0])); + } + + term->search.buf[term->search.len] = U'\0'; + free(new_text); + + if (move_cursor) + term->search.cursor += actually_copied; + + struct range match = {.start = *target, .end = selection_get_end(term)}; + search_update_selection(term, &match); + + term->search.match_len = term->search.len; +} + +static void +search_extend_right(struct terminal *term, const struct coord *target) +{ + if (term->search.match_len == 0) + return; + + struct coord pos = selection_get_end(term); + const struct row *row = term->grid->rows[pos.row]; + + const bool move_cursor = term->search.cursor == term->search.len; + + struct extraction_context *ctx = extract_begin(SELECTION_NONE, false); + if (ctx == NULL) + return; + + do { + if (!coord_advance_right(term, &pos, &row)) + break; + if (!extract_one(term, row, &row->cells[pos.col], pos.col, ctx)) + break; + } while (pos.col != target->col || pos.row != target->row); + + char32_t *new_text; + size_t new_len; + + if (!extract_finish_wide(ctx, &new_text, &new_len)) + return; + + if (!search_ensure_size(term, term->search.len + new_len)) + return; + + for (size_t i = 0; i < new_len; i++) { + if (new_text[i] == U'\n') { + /* extract() adds newlines, which we never match against */ + continue; + } + + term->search.buf[term->search.len++] = new_text[i]; + } + + term->search.buf[term->search.len] = U'\0'; + free(new_text); + + if (move_cursor) + term->search.cursor = term->search.len; + + struct range match = {.start = term->search.match, .end = *target}; + search_update_selection(term, &match); + term->search.match_len = term->search.len; +} + +static size_t +distance_next_word(const struct terminal *term) +{ + size_t cursor = term->search.cursor; + + /* First eat non-whitespace. This is the word we're skipping past */ + while (cursor < term->search.len) { + if (isc32space(term->search.buf[cursor++])) + break; + } + + xassert(cursor == term->search.len || isc32space(term->search.buf[cursor - 1])); + + /* Now skip past whitespace, so that we end up at the beginning of + * the next word */ + while (cursor < term->search.len) { + if (!isc32space(term->search.buf[cursor++])) + break; + } + + xassert(cursor == term->search.len || !isc32space(term->search.buf[cursor - 1])); + + if (cursor < term->search.len && !isc32space(term->search.buf[cursor])) + cursor--; + + return cursor - term->search.cursor; +} + +static size_t +distance_prev_word(const struct terminal *term) +{ + int cursor = term->search.cursor; + + /* First, eat whitespace prefix */ + while (cursor > 0) { + if (!isc32space(term->search.buf[--cursor])) + break; + } + + xassert(cursor == 0 || !isc32space(term->search.buf[cursor])); + + /* Now eat non-whitespace. This is the word we're skipping past */ + while (cursor > 0) { + if (isc32space(term->search.buf[--cursor])) + break; + } + + xassert(cursor == 0 || isc32space(term->search.buf[cursor])); + if (cursor > 0 && isc32space(term->search.buf[cursor])) + cursor++; + + return term->search.cursor - cursor; +} + +static void +from_clipboard_cb(char *text, size_t size, void *user) +{ + struct terminal *term = user; + search_add_chars(term, text, size); +} + +static void +from_clipboard_done(void *user) +{ + struct terminal *term = user; + + LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf); + search_find_next(term, SEARCH_BACKWARD_SAME_POSITION); + render_refresh_search(term); +} + +static bool +execute_binding(struct seat *seat, struct terminal *term, + const struct key_binding *binding, uint32_t serial, + bool *update_search_result, enum search_direction *direction, + bool *redraw) +{ + *update_search_result = *redraw = false; + const enum bind_action_search action = binding->action; + + struct grid *grid = term->grid; + + switch (action) { + case BIND_ACTION_SEARCH_NONE: + return false; + + case BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->rows); + return true; + } + return false; + + case BIND_ACTION_SEARCH_SCROLLBACK_UP_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_UP_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, 1); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->rows); + return true; + } + return false; + + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_HALF_PAGE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, max(term->rows / 2, 1)); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_DOWN_LINE: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, 1); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_HOME: + if (term->grid == &term->normal) { + cmd_scrollback_up(term, term->grid->num_rows); + return true; + } + break; + + case BIND_ACTION_SEARCH_SCROLLBACK_END: + if (term->grid == &term->normal) { + cmd_scrollback_down(term, term->grid->num_rows); + return true; + } + break; + + case BIND_ACTION_SEARCH_CANCEL: + if (term->search.view_followed_offset) + grid->view = grid->offset; + else { + grid->view = ensure_view_is_allocated( + term, term->search.original_view); + } + term_damage_view(term); + search_cancel(term); + return true; + + case BIND_ACTION_SEARCH_COMMIT: + selection_finalize(seat, term, serial); + search_cancel_keep_selection(term); + return true; + + case BIND_ACTION_SEARCH_FIND_PREV: + if (term->search.last.buf != NULL && term->search.len == 0) { + add_wchars(term, term->search.last.buf, term->search.last.len); + + free(term->search.last.buf); + term->search.last.buf = NULL; + term->search.last.len = 0; + } + + *direction = SEARCH_BACKWARD; + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_FIND_NEXT: + if (term->search.last.buf != NULL && term->search.len == 0) { + add_wchars(term, term->search.last.buf, term->search.last.len); + + free(term->search.last.buf); + term->search.last.buf = NULL; + term->search.last.len = 0; + } + + *direction = SEARCH_FORWARD; + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_EDIT_LEFT: + if (term->search.cursor > 0) { + term->search.cursor--; + *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_EDIT_LEFT_WORD: { + size_t diff = distance_prev_word(term); + term->search.cursor -= diff; + xassert(term->search.cursor <= term->search.len); + + if (diff > 0) + *redraw = true; + return true; + } + + case BIND_ACTION_SEARCH_EDIT_RIGHT: + if (term->search.cursor < term->search.len) { + term->search.cursor++; + *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_EDIT_RIGHT_WORD: { + size_t diff = distance_next_word(term); + term->search.cursor += diff; + xassert(term->search.cursor <= term->search.len); + + if (diff > 0) + *redraw = true; + return true; + } + + case BIND_ACTION_SEARCH_EDIT_HOME: + if (term->search.cursor != 0) { + term->search.cursor = 0; + *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_EDIT_END: + if (term->search.cursor != term->search.len) { + term->search.cursor = term->search.len; + *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_DELETE_PREV: + if (term->search.cursor > 0) { + memmove( + &term->search.buf[term->search.cursor - 1], + &term->search.buf[term->search.cursor], + (term->search.len - term->search.cursor) * sizeof(char32_t)); + term->search.cursor--; + term->search.buf[--term->search.len] = U'\0'; + *update_search_result = *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_DELETE_PREV_WORD: { + size_t diff = distance_prev_word(term); + size_t old_cursor = term->search.cursor; + size_t new_cursor = old_cursor - diff; + + if (diff > 0) { + memmove(&term->search.buf[new_cursor], + &term->search.buf[old_cursor], + (term->search.len - old_cursor) * sizeof(char32_t)); + + term->search.len -= diff; + term->search.cursor = new_cursor; + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_DELETE_NEXT: + if (term->search.cursor < term->search.len) { + memmove( + &term->search.buf[term->search.cursor], + &term->search.buf[term->search.cursor + 1], + (term->search.len - term->search.cursor - 1) * sizeof(char32_t)); + term->search.buf[--term->search.len] = U'\0'; + *update_search_result = *redraw = true; + } + return true; + + case BIND_ACTION_SEARCH_DELETE_NEXT_WORD: { + size_t diff = distance_next_word(term); + size_t cursor = term->search.cursor; + + if (diff > 0) { + memmove(&term->search.buf[cursor], + &term->search.buf[cursor + diff], + (term->search.len - (cursor + diff)) * sizeof(char32_t)); + + term->search.len -= diff; + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_DELETE_TO_START: { + if (term->search.cursor > 0) { + memmove(&term->search.buf[0], + &term->search.buf[term->search.cursor], + (term->search.len - term->search.cursor) + * sizeof(char32_t)); + + term->search.len -= term->search.cursor; + term->search.cursor = 0; + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_DELETE_TO_END: { + if (term->search.cursor < term->search.len) { + term->search.buf[term->search.cursor] = '\0'; + term->search.len = term->search.cursor; + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_CHAR: { + struct coord target; + if (search_extend_find_char_right(term, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_WORD: { + struct coord target; + if (search_extend_find_word_right(term, false, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_WORD_WS: { + struct coord target; + if (search_extend_find_word_right(term, true, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_LINE_DOWN: { + struct coord target; + if (search_extend_find_line_down(term, &target)) { + search_extend_right(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR: { + struct coord target; + if (search_extend_find_char_left(term, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD: { + struct coord target; + if (search_extend_find_word_left(term, false, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD_WS: { + struct coord target; + if (search_extend_find_word_left(term, true, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_EXTEND_LINE_UP: { + struct coord target; + if (search_extend_find_line_up(term, &target)) { + search_extend_left(term, &target); + *update_search_result = false; + *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_CLIPBOARD_PASTE: + text_from_clipboard( + seat, term, false, &from_clipboard_cb, &from_clipboard_done, term); + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_PRIMARY_PASTE: + text_from_primary( + seat, term, false, &from_clipboard_cb, &from_clipboard_done, term); + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_UNICODE_INPUT: + unicode_mode_activate(term); + return true; + + case BIND_ACTION_SEARCH_TOGGLE_CASE: + term->search.case_mode = (term->search.case_mode + 1) % 3; + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_TOGGLE_WHOLE_WORD: + term->search.whole_word = !term->search.whole_word; + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_TOGGLE_REGEX: + term->search.regex = !term->search.regex; + *update_search_result = *redraw = true; + return true; + + case BIND_ACTION_SEARCH_HISTORY_PREV: { + struct search_history_entry *target; + if (term->search.history_pos == NULL) + target = term->search.history_tail; + else + target = term->search.history_pos->prev; + + if (target != NULL) { + term->search.history_pos = target; + term->search.len = 0; + term->search.cursor = 0; + if (term->search.buf != NULL) + term->search.buf[0] = U'\0'; + add_wchars(term, target->buf, target->len); + *update_search_result = *redraw = true; + } + return true; + } + + case BIND_ACTION_SEARCH_HISTORY_NEXT: { + if (term->search.history_pos == NULL) + return true; + + struct search_history_entry *target = term->search.history_pos->next; + term->search.history_pos = target; + + term->search.len = 0; + term->search.cursor = 0; + if (term->search.buf != NULL) + term->search.buf[0] = U'\0'; + + if (target != NULL) + add_wchars(term, target->buf, target->len); + + *update_search_result = *redraw = true; + return true; + } + + case BIND_ACTION_SEARCH_COMMIT_LINE: { + if (term->search.match_len == 0) { + /* No match — fall back to plain commit */ + selection_finalize(seat, term, serial); + search_cancel_keep_selection(term); + return true; + } + + /* Extend selection to span entire line(s) of the match */ + const struct coord match_end = selection_get_end(term); + const int start_row = term->search.match.row; + const int end_row = match_end.row; + + int sel_start_row = start_row - grid->view + grid->num_rows; + sel_start_row &= grid->num_rows - 1; + + int sel_end_row = end_row - grid->view + grid->num_rows; + sel_end_row &= grid->num_rows - 1; + + selection_cancel(term); + selection_start(term, 0, sel_start_row, SELECTION_CHAR_WISE, false); + selection_update(term, term->cols - 1, sel_end_row); + selection_finalize(seat, term, serial); + search_cancel_keep_selection(term); + return true; + } + + case BIND_ACTION_SEARCH_COUNT: + BUG("Invalid action type"); + return true; + } + + BUG("Unhandled action type"); + return false; +} + +void +search_input(struct seat *seat, struct terminal *term, + const struct key_binding_set *bindings, uint32_t key, + xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, + const xkb_keysym_t *raw_syms, size_t raw_count, + uint32_t serial) +{ + LOG_DBG("search: input: sym=%d/0x%x, mods=0x%08x, consumed=0x%08x", + sym, sym, mods, consumed); + + enum xkb_compose_status compose_status = seat->kbd.xkb_compose_state != NULL + ? xkb_compose_state_get_status(seat->kbd.xkb_compose_state) + : XKB_COMPOSE_NOTHING; + + enum search_direction search_direction = SEARCH_BACKWARD_SAME_POSITION; + bool update_search_result = false; + bool redraw = false; + + /* + * Key bindings + */ + + /* Match untranslated symbols */ + tll_foreach(bindings->search, it) { + const struct key_binding *bind = &it->item; + + if (bind->mods != mods || bind->mods == 0) + continue; + + for (size_t i = 0; i < raw_count; i++) { + if (bind->k.sym == raw_syms[i]) { + if (execute_binding(seat, term, bind, serial, + &update_search_result, &search_direction, + &redraw)) + { + goto update_search; + } + return; + } + } + } + + /* Match translated symbol */ + tll_foreach(bindings->search, it) { + const struct key_binding *bind = &it->item; + + if (bind->k.sym == sym && + bind->mods == (mods & ~consumed)) { + + if (execute_binding(seat, term, bind, serial, + &update_search_result, &search_direction, + &redraw)) + { + goto update_search; + } + return; + } + } + + /* Match raw key code */ + tll_foreach(bindings->search, it) { + const struct key_binding *bind = &it->item; + + if (bind->mods != mods || bind->mods == 0) + continue; + + tll_foreach(bind->k.key_codes, code) { + if (code->item == key) { + if (execute_binding(seat, term, bind, serial, + &update_search_result, &search_direction, + &redraw)) + { + goto update_search; + } + return; + } + } + } + + uint8_t buf[64] = {0}; + int count = 0; + + if (compose_status == XKB_COMPOSE_COMPOSED) { + count = xkb_compose_state_get_utf8( + seat->kbd.xkb_compose_state, (char *)buf, sizeof(buf)); + xkb_compose_state_reset(seat->kbd.xkb_compose_state); + } else if (compose_status == XKB_COMPOSE_CANCELLED || + compose_status == XKB_COMPOSE_COMPOSING) { + count = 0; + } else { + count = xkb_state_key_get_utf8( + seat->kbd.xkb_state, key, (char *)buf, sizeof(buf)); + } + + update_search_result = redraw = count > 0; + search_direction = SEARCH_BACKWARD_SAME_POSITION; + + if (count == 0) + return; + + search_add_chars(term, (const char *)buf, count); + +update_search: + LOG_DBG("search: buffer: %ls", (const wchar_t *)term->search.buf); + if (update_search_result) + search_find_next(term, search_direction); + if (redraw) + render_refresh_search(term); +} diff --git a/search.h b/search.h new file mode 100644 index 0000000..ee8ecd7 --- /dev/null +++ b/search.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "key-binding.h" +#include "terminal.h" + +void search_begin(struct terminal *term); +void search_cancel(struct terminal *term); +void search_input( + struct seat *seat, struct terminal *term, + const struct key_binding_set *bindings, uint32_t key, + xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, + const xkb_keysym_t *raw_syms, size_t raw_count, + uint32_t serial); +void search_add_chars(struct terminal *term, const char *text, size_t len); + +void search_selection_cancelled(struct terminal *term); + +struct search_match_iterator { + struct terminal *term; + struct coord start; +}; + +struct search_match_iterator search_matches_new_iter(struct terminal *term); +struct range search_matches_next(struct search_match_iterator *iter); diff --git a/selection.c b/selection.c new file mode 100644 index 0000000..0a479ee --- /dev/null +++ b/selection.c @@ -0,0 +1,2919 @@ +#include "selection.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#define LOG_MODULE "selection" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "async.h" +#include "char32.h" +#include "commands.h" +#include "config.h" +#include "debug.h" +#include "extract.h" +#include "grid.h" +#include "misc.h" +#include "render.h" +#include "search.h" +#include "uri.h" +#include "util.h" +#include "vt.h" +#include "xmalloc.h" + +static const char *const mime_type_map[] = { + [DATA_OFFER_MIME_UNSET] = NULL, + [DATA_OFFER_MIME_TEXT_PLAIN] = "text/plain", + [DATA_OFFER_MIME_TEXT_UTF8] = "text/plain;charset=utf-8", + [DATA_OFFER_MIME_URI_LIST] = "text/uri-list", + + [DATA_OFFER_MIME_TEXT_TEXT] = "TEXT", + [DATA_OFFER_MIME_TEXT_STRING] = "STRING", + [DATA_OFFER_MIME_TEXT_UTF8_STRING] = "UTF8_STRING", +}; + +static inline struct coord +bounded(const struct grid *grid, struct coord coord) +{ + coord.row &= grid->num_rows - 1; + return coord; +} + +struct coord +selection_get_start(const struct terminal *term) +{ + if (term->selection.coords.start.row < 0) + return term->selection.coords.start; + return bounded(term->grid, term->selection.coords.start); +} + +struct coord +selection_get_end(const struct terminal *term) +{ + if (term->selection.coords.end.row < 0) + return term->selection.coords.end; + return bounded(term->grid, term->selection.coords.end); +} + +bool +selection_on_rows(const struct terminal *term, int row_start, int row_end) +{ + xassert(term->selection.coords.end.row >= 0); + + LOG_DBG("on rows: %d-%d, range: %d-%d (offset=%d)", + term->selection.coords.start.row, term->selection.coords.end.row, + row_start, row_end, term->grid->offset); + + row_start += term->grid->offset; + row_end += term->grid->offset; + xassert(row_end >= row_start); + + const struct coord *start = &term->selection.coords.start; + const struct coord *end = &term->selection.coords.end; + + const struct grid *grid = term->grid; + const int sb_start = grid->offset + term->rows; + + /* Use scrollback relative coords when checking for overlap */ + const int rel_row_start = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, row_start); + const int rel_row_end = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, row_end); + int rel_sel_start = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, start->row); + int rel_sel_end = + grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, end->row); + + if (rel_sel_start > rel_sel_end) { + int tmp = rel_sel_start; + rel_sel_start = rel_sel_end; + rel_sel_end = tmp; + } + + if ((rel_row_start <= rel_sel_start && rel_row_end >= rel_sel_start) || + (rel_row_start <= rel_sel_end && rel_row_end >= rel_sel_end)) + { + /* The range crosses one of the selection boundaries */ + return true; + } + + if (rel_row_start >= rel_sel_start && rel_row_end <= rel_sel_end) + return true; + + return false; +} + +void +selection_scroll_up(struct terminal *term, int rows) +{ + xassert(term->selection.coords.end.row >= 0); + + const int rel_row_start = + grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.start.row); + const int rel_row_end = + grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.end.row); + const int actual_start = min(rel_row_start, rel_row_end); + + if (actual_start - rows < 0) { + /* Part of the selection will be scrolled out, cancel it */ + selection_cancel(term); + } +} + +void +selection_scroll_down(struct terminal *term, int rows) +{ + xassert(term->selection.coords.end.row >= 0); + + const struct grid *grid = term->grid; + const struct range *sel = &term->selection.coords; + + const int screen_end = + grid_row_abs_to_sb(grid, term->rows, grid->offset + term->rows - 1); + const int rel_row_start = + grid_row_abs_to_sb(term->grid, term->rows, sel->start.row); + const int rel_row_end = + grid_row_abs_to_sb(term->grid, term->rows, sel->end.row); + const int actual_end = max(rel_row_start, rel_row_end); + + if (actual_end > screen_end - rows) { + /* Part of the selection will be scrolled out, cancel it */ + selection_cancel(term); + } +} + +void +selection_view_up(struct terminal *term, int new_view) +{ + if (likely(term->selection.coords.start.row < 0)) + return; + + if (likely(new_view < term->grid->view)) + return; + + term->selection.coords.start.row += term->grid->num_rows; + if (term->selection.coords.end.row >= 0) + term->selection.coords.end.row += term->grid->num_rows; +} + +void +selection_view_down(struct terminal *term, int new_view) +{ + if (likely(term->selection.coords.start.row < 0)) + return; + + if (likely(new_view > term->grid->view)) + return; + + term->selection.coords.start.row &= term->grid->num_rows - 1; + if (term->selection.coords.end.row >= 0) + term->selection.coords.end.row &= term->grid->num_rows - 1; +} + +static void +foreach_selected_normal( + struct terminal *term, struct coord _start, struct coord _end, + bool (*cb)(struct terminal *term, struct row *row, struct cell *cell, + int row_no, int col, void *data), + void *data) +{ + const struct coord *start = &_start; + const struct coord *end = &_end; + + const int grid_rows = term->grid->num_rows; + + /* Start/end rows, relative to the scrollback start */ + /* Start/end rows, relative to the scrollback start */ + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); + + int start_row, end_row; + int start_col, end_col; + + if (rel_start_row < rel_end_row) { + start_row = start->row; + start_col = start->col; + end_row = end->row; + end_col = end->col; + } else if (rel_start_row > rel_end_row) { + start_row = end->row; + start_col = end->col; + end_row = start->row; + end_col = start->col; + } else { + start_row = end_row = start->row; + start_col = min(start->col, end->col); + end_col = max(start->col, end->col); + } + + start_row &= (grid_rows - 1); + end_row &= (grid_rows - 1); + + for (int r = start_row; r != end_row; r = (r + 1) & (grid_rows - 1)) { + struct row *row = term->grid->rows[r]; + xassert(row != NULL); + + for (int c = start_col; c <= term->cols - 1; c++) { + if (!cb(term, row, &row->cells[c], r, c, data)) + return; + } + + start_col = 0; + } + + /* Last, partial row */ + struct row *row = term->grid->rows[end_row]; + xassert(row != NULL); + + for (int c = start_col; c <= end_col; c++) { + if (!cb(term, row, &row->cells[c], end_row, c, data)) + return; + } +} + +static void +foreach_selected_block( + struct terminal *term, struct coord _start, struct coord _end, + bool (*cb)(struct terminal *term, struct row *row, struct cell *cell, + int row_no, int col, void *data), + void *data) +{ + const struct coord *start = &_start; + const struct coord *end = &_end; + + const int grid_rows = term->grid->num_rows; + + /* Start/end rows, relative to the scrollback start */ + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); + + struct coord top_left = { + .row = (rel_start_row < rel_end_row + ? start->row : end->row) & (grid_rows - 1), + .col = min(start->col, end->col), + }; + + struct coord bottom_right = { + .row = (rel_start_row > rel_end_row + ? start->row : end->row) & (grid_rows - 1), + .col = max(start->col, end->col), + }; + + int r = top_left.row; + while (true) { + struct row *row = term->grid->rows[r]; + xassert(row != NULL); + + for (int c = top_left.col; c <= bottom_right.col; c++) { + if (!cb(term, row, &row->cells[c], r, c, data)) + return; + } + + if (r == bottom_right.row) + break; + + r++; + r &= grid_rows - 1; + } +} + +static void +foreach_selected( + struct terminal *term, struct coord start, struct coord end, + bool (*cb)(struct terminal *term, struct row *row, struct cell *cell, int row_no, int col, void *data), + void *data) +{ + switch (term->selection.kind) { + case SELECTION_CHAR_WISE: + case SELECTION_WORD_WISE: + case SELECTION_QUOTE_WISE: + case SELECTION_LINE_WISE: + foreach_selected_normal(term, start, end, cb, data); + return; + + case SELECTION_BLOCK: + foreach_selected_block(term, start, end, cb, data); + return; + + case SELECTION_NONE: + break; + } + + BUG("Invalid selection kind"); +} + +static bool +extract_one_const_wrapper(struct terminal *term, + struct row *row, struct cell *cell, + int row_no, int col, void *data) +{ + return extract_one(term, row, cell, col, data); +} + +char * +selection_to_text(const struct terminal *term) +{ + if (term->selection.coords.end.row == -1) + return NULL; + + struct extraction_context *ctx = extract_begin(term->selection.kind, true); + if (ctx == NULL) + return NULL; + + foreach_selected( + (struct terminal *)term, term->selection.coords.start, term->selection.coords.end, + &extract_one_const_wrapper, ctx); + + char *text; + return extract_finish(ctx, &text, NULL) ? text : NULL; +} + +/* Coordinates are in *absolute* row numbers (NOT view local) */ +void +selection_find_word_boundary_left(const struct terminal *term, struct coord *pos, + bool spaces_only) +{ + const struct grid *grid = term->grid; + + xassert(pos->col >= 0); + xassert(pos->col < term->cols); + xassert(pos->row >= 0); + pos->row &= grid->num_rows - 1; + + const struct row *r = grid->rows[pos->row]; + char32_t c = r->cells[pos->col].wc; + + while (c >= CELL_SPACER) { + xassert(pos->col > 0); + if (pos->col == 0) + return; + pos->col--; + c = r->cells[pos->col].wc; + } + + if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI) + c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0]; + + bool initial_is_space = c == 0 || isc32space(c); + bool initial_is_delim = + !initial_is_space && !isword(c, spaces_only, term->conf->word_delimiters); + bool initial_is_word = + c != 0 && isword(c, spaces_only, term->conf->word_delimiters); + + while (true) { + int next_col = pos->col - 1; + int next_row = pos->row; + + const struct row *row = grid->rows[next_row]; + + /* Linewrap */ + if (next_col < 0) { + next_col = term->cols - 1; + + next_row = (next_row - 1 + grid->num_rows) & (grid->num_rows - 1); + + if (grid_row_abs_to_sb(grid, term->rows, next_row) == term->grid->num_rows - 1 || + grid->rows[next_row] == NULL) + { + /* Scrollback wrap-around */ + break; + } + + row = grid->rows[next_row]; + + if (row->linebreak) { + /* Hard linebreak, treat as space. I.e. break selection */ + break; + } + } + + c = row->cells[next_col].wc; + while (c >= CELL_SPACER) { + xassert(next_col > 0); + if (--next_col < 0) + return; + c = row->cells[next_col].wc; + } + + if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI) + c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0]; + + bool is_space = c == 0 || isc32space(c); + bool is_delim = + !is_space && !isword(c, spaces_only, term->conf->word_delimiters); + bool is_word = + c != 0 && isword(c, spaces_only, term->conf->word_delimiters); + + if (initial_is_space && !is_space) + break; + if (initial_is_delim && !is_delim) + break; + if (initial_is_word && !is_word) + break; + + pos->col = next_col; + pos->row = next_row; + } +} + +/* Coordinates are in *absolute* row numbers (NOT view local) */ +void +selection_find_word_boundary_right(const struct terminal *term, struct coord *pos, + bool spaces_only, + bool stop_on_space_to_word_boundary) +{ + const struct grid *grid = term->grid; + + xassert(pos->col >= 0); + xassert(pos->col < term->cols); + xassert(pos->row >= 0); + pos->row &= grid->num_rows - 1; + + const struct row *r = grid->rows[pos->row]; + char32_t c = r->cells[pos->col].wc; + + while (c >= CELL_SPACER) { + xassert(pos->col > 0); + if (pos->col == 0) + return; + pos->col--; + c = r->cells[pos->col].wc; + } + + if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI) + c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0]; + + bool initial_is_space = c == 0 || isc32space(c); + bool initial_is_delim = + !initial_is_space && !isword(c, spaces_only, term->conf->word_delimiters); + bool initial_is_word = + c != 0 && isword(c, spaces_only, term->conf->word_delimiters); + bool have_seen_word = initial_is_word; + + while (true) { + int next_col = pos->col + 1; + int next_row = pos->row; + + const struct row *row = term->grid->rows[next_row]; + + /* Linewrap */ + if (next_col >= term->cols) { + if (row->linebreak) { + /* Hard linebreak, treat as space. I.e. break selection */ + break; + } + + next_col = 0; + next_row = (next_row + 1) & (grid->num_rows - 1); + + if (grid_row_abs_to_sb(grid, term->rows, next_row) == 0) { + /* Scrollback wrap-around */ + break; + } + + row = grid->rows[next_row]; + } + + c = row->cells[next_col].wc; + while (c >= CELL_SPACER) { + if (++next_col >= term->cols) { + next_col = 0; + if (++next_row >= term->rows) + return; + } + c = row->cells[next_col].wc; + } + + if (c >= CELL_COMB_CHARS_LO && c <= CELL_COMB_CHARS_HI) + c = composed_lookup(term->composed, c - CELL_COMB_CHARS_LO)->chars[0]; + + bool is_space = c == 0 || isc32space(c); + bool is_delim = + !is_space && !isword(c, spaces_only, term->conf->word_delimiters); + bool is_word = + c != 0 && isword(c, spaces_only, term->conf->word_delimiters); + + if (stop_on_space_to_word_boundary) { + if (initial_is_space && !is_space) + break; + if (initial_is_delim && !is_delim) + break; + } else { + if (initial_is_space && ((have_seen_word && is_space) || is_delim)) + break; + if (initial_is_delim && ((have_seen_word && is_delim) || is_space)) + break; + } + if (initial_is_word && !is_word) + break; + + have_seen_word = is_word; + + pos->col = next_col; + pos->row = next_row; + } +} + +static bool +selection_find_quote_left(struct terminal *term, struct coord *pos, + char32_t *quote_char) +{ + const struct row *row = grid_row_in_view(term->grid, pos->row); + char32_t wc = row->cells[pos->col].wc; + + if (*quote_char == '\0' ? (wc == '"' || wc == '\'') + : wc == *quote_char) + { + return false; + } + + int next_row = pos->row; + int next_col = pos->col; + + while (true) { + if (--next_col < 0) { + next_col = term->cols - 1; + if (--next_row < 0) + return false; + + row = grid_row_in_view(term->grid, next_row); + if (row->linebreak) + return false; + } + + wc = row->cells[next_col].wc; + + if (*quote_char == '\0' ? (wc == '"' || wc == '\'') + : wc == *quote_char) + { + xassert(next_col + 1 <= term->cols); + if (next_col + 1 == term->cols) { + xassert(next_row < pos->row); + pos->row = next_row + 1; + pos->col = 0; + } else { + pos->row = next_row; + pos->col = next_col + 1; + } + + *quote_char = wc; + return true; + } + } +} + +static bool +selection_find_quote_right(struct terminal *term, struct coord *pos, char32_t quote_char) +{ + if (quote_char == '\0') + return false; + + const struct row *row = grid_row_in_view(term->grid, pos->row); + char32_t wc = row->cells[pos->col].wc; + if (wc == quote_char) + return false; + + int next_row = pos->row; + int next_col = pos->col; + + while (true) { + if (++next_col >= term->cols) { + next_col = 0; + if (++next_row >= term->rows) + return false; + + if (row->linebreak) + return false; + + row = grid_row_in_view(term->grid, next_row); + } + + wc = row->cells[next_col].wc; + if (wc == quote_char) { + pos->row = next_row; + pos->col = next_col - 1; + xassert(pos->col >= 0); + return true; + } + } +} + +static void +selection_find_line_boundary_left(struct terminal *term, struct coord *pos) +{ + int next_row = pos->row; + pos->col = 0; + + while (true) { + if (--next_row < 0) + return; + + const struct row *row = grid_row_in_view(term->grid, next_row); + assert(row != NULL); + + if (row->linebreak) + return; + + pos->col = 0; + pos->row = next_row; + } +} + +static void +selection_find_line_boundary_right(struct terminal *term, struct coord *pos) +{ + int next_row = pos->row; + pos->col = term->cols - 1; + + while (true) { + const struct row *row = grid_row_in_view(term->grid, next_row); + assert(row != NULL); + + if (row->linebreak) + return; + + if (++next_row >= term->rows) + return; + + pos->col = term->cols - 1; + pos->row = next_row; + } +} + +void +selection_start(struct terminal *term, int col, int row, + enum selection_kind kind, + bool spaces_only) +{ + selection_cancel(term); + + LOG_DBG("%s selection started at %d,%d", + kind == SELECTION_CHAR_WISE ? "character-wise" : + kind == SELECTION_WORD_WISE ? "word-wise" : + kind == SELECTION_QUOTE_WISE ? "quote-wise" : + kind == SELECTION_LINE_WISE ? "line-wise" : + kind == SELECTION_BLOCK ? "block" : "", + row, col); + + term->selection.kind = kind; + term->selection.ongoing = true; + term->selection.spaces_only = spaces_only; + + switch (kind) { + case SELECTION_CHAR_WISE: + case SELECTION_BLOCK: + term->selection.coords.start = (struct coord){col, term->grid->view + row}; + term->selection.coords.end = (struct coord){-1, -1}; + + term->selection.pivot.start = term->selection.coords.start; + term->selection.pivot.end = term->selection.coords.end; + break; + + case SELECTION_WORD_WISE: { + struct coord start = {col, term->grid->view + row}; + struct coord end = {col, term->grid->view + row}; + selection_find_word_boundary_left(term, &start, spaces_only); + selection_find_word_boundary_right(term, &end, spaces_only, true); + + term->selection.coords.start = start; + + term->selection.pivot.start = term->selection.coords.start; + term->selection.pivot.end = end; + + /* + * FIXME: go through selection.c and make sure all public + * functions use the *same* coordinate system... + * + * selection_find_word_boundary*() uses absolute row numbers, + * while selection_update(), and pretty much all others, use + * view-local. + */ + + selection_update(term, end.col, end.row - term->grid->view); + break; + } + + case SELECTION_QUOTE_WISE: { + struct coord start = {col, row}, end = {col, row}; + + char32_t quote_char = '\0'; + bool found_left = selection_find_quote_left(term, &start, "e_char); + bool found_right = selection_find_quote_right(term, &end, quote_char); + + if (found_left && !found_right) { + xassert(quote_char != '\0'); + + /* + * Try to flip the quote character we're looking for. + * + * This lets us handle things like: + * + * "nested 'quotes are fun', right" + * + * In the example above, starting the selection at + * "right", will otherwise not match. find-left will find + * the single quote, causing find-right to fail. + * + * By flipping the quote-character, and re-trying, we + * find-left will find the starting double quote, letting + * find-right succeed as well. + */ + + if (quote_char == '\'') + quote_char = '"'; + else if (quote_char == '"') + quote_char = '\''; + + found_left = selection_find_quote_left(term, &start, "e_char); + found_right = selection_find_quote_right(term, &end, quote_char); + } + + if (found_left && found_right) { + term->selection.coords.start = (struct coord){ + start.col, term->grid->view + start.row}; + + term->selection.pivot.start = term->selection.coords.start; + term->selection.pivot.end = (struct coord){end.col, term->grid->view + end.row}; + + term->selection.kind = SELECTION_WORD_WISE; + selection_update(term, end.col, end.row); + break; + } else { + term->selection.kind = SELECTION_LINE_WISE; + /* FALLTHROUGH */ + } + } + + case SELECTION_LINE_WISE: { + struct coord start = {0, row}, end = {term->cols - 1, row}; + selection_find_line_boundary_left(term, &start); + selection_find_line_boundary_right(term, &end); + + term->selection.coords.start = (struct coord){ + start.col, term->grid->view + start.row}; + term->selection.pivot.start = term->selection.coords.start; + term->selection.pivot.end = (struct coord){end.col, term->grid->view + end.row}; + + selection_update(term, end.col, end.row); + break; + } + + case SELECTION_NONE: + BUG("Invalid selection kind"); + break; + } + +} + +static pixman_region32_t +pixman_region_for_coords_normal(const struct terminal *term, + const struct coord *start, + const struct coord *end) +{ + pixman_region32_t region; + pixman_region32_init(®ion); + + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); + + if (rel_start_row < rel_end_row) { + /* First partial row (start ->)*/ + pixman_region32_union_rect( + ®ion, ®ion, + start->col, rel_start_row, + term->cols - start->col, 1); + + /* Full rows between start and end */ + if (rel_start_row + 1 < rel_end_row) { + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_start_row + 1, + term->cols, rel_end_row - rel_start_row - 1); + } + + /* Last partial row (-> end) */ + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_end_row, + end->col + 1, 1); + + } else if (rel_start_row > rel_end_row) { + /* First partial row (end ->) */ + pixman_region32_union_rect( + ®ion, ®ion, + end->col, rel_end_row, + term->cols - end->col, 1); + + /* Full rows between end and start */ + if (rel_end_row + 1 < rel_start_row) { + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_end_row + 1, + term->cols, rel_start_row - rel_end_row - 1); + } + + /* Last partial row (-> start) */ + pixman_region32_union_rect( + ®ion, ®ion, + 0, rel_start_row, + start->col + 1, 1); + } else { + const int start_col = min(start->col, end->col); + const int end_col = max(start->col, end->col); + + pixman_region32_union_rect( + ®ion, ®ion, + start_col, rel_start_row, + end_col + 1 - start_col, 1); + } + + return region; +} + +static pixman_region32_t +pixman_region_for_coords_block(const struct terminal *term, + const struct coord *start, const struct coord *end) +{ + pixman_region32_t region; + pixman_region32_init(®ion); + + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); + + pixman_region32_union_rect( + ®ion, ®ion, + min(start->col, end->col), min(rel_start_row, rel_end_row), + abs(start->col - end->col) + 1, abs(rel_start_row - rel_end_row) + 1); + + return region; +} + +/* Returns a pixman region representing the selection between 'start' + * and 'end' (given the current selection kind), in *scrollback + * relative coordinates* */ +static pixman_region32_t +pixman_region_for_coords(const struct terminal *term, + const struct coord *start, const struct coord *end) +{ + switch (term->selection.kind) { + default: return pixman_region_for_coords_normal(term, start, end); + case SELECTION_BLOCK: return pixman_region_for_coords_block(term, start, end); + } +} + +enum mark_selection_variant { + MARK_SELECTION_MARK_AND_DIRTY, + MARK_SELECTION_UNMARK_AND_DIRTY, + MARK_SELECTION_MARK_FOR_RENDER, +}; + +static void +mark_selected_region(struct terminal *term, pixman_box32_t *boxes, + size_t count, enum mark_selection_variant mark_variant) +{ + const bool selected = + mark_variant == MARK_SELECTION_MARK_AND_DIRTY || + mark_variant == MARK_SELECTION_MARK_FOR_RENDER; + const bool dirty_cells = + mark_variant == MARK_SELECTION_MARK_AND_DIRTY || + mark_variant == MARK_SELECTION_UNMARK_AND_DIRTY; + const bool highlight_empty = + mark_variant != MARK_SELECTION_MARK_FOR_RENDER || + term->selection.kind == SELECTION_BLOCK; + + for (size_t i = 0; i < count; i++) { + const pixman_box32_t *box = &boxes[i]; + + LOG_DBG("%s selection in region: %dx%d - %dx%d", + selected ? "marking" : "unmarking", + box->x1, box->y1, + box->x2, box->y2); + + int abs_row_start = grid_row_sb_to_abs( + term->grid, term->rows, box->y1); + + for (int r = abs_row_start, rel_r = box->y1; + rel_r < box->y2; + r = (r + 1) & (term->grid->num_rows - 1), rel_r++) + { + struct row *row = term->grid->rows[r]; + xassert(row != NULL); + + if (dirty_cells) + row->dirty = true; + + for (int c = box->x1, empty_count = 0; c < box->x2; c++) { + struct cell *cell = &row->cells[c]; + + if (cell->wc == 0 && !highlight_empty) { + /* + * We used to highlight empty cells *if* they were + * followed by non-empty cell(s), since this + * corresponds to what gets extracted when the + * selection is copied (that is, empty cells + * "between" non-empty cells are converted to + * spaces). + * + * However, they way we handle selection updates + * (diffing the "old" selection area against the + * "new" one, using pixman regions), means we + * can't correctly update the state of empty + * cells. The result is "random" empty cells being + * rendered as selected when they shouldn't. + * + * "Fix" by *never* highlighting selected empty + * cells (they still get converted to spaces when + * copied, if followed by non-empty cells). + */ + empty_count++; + + /* + * When the selection is *modified*, empty cells + * are treated just like non-empty cells; they are + * marked as selected, and dirtied. + * + * This is due to how the algorithm for updating + * the selection works; it uses regions to + * calculate the difference between the "old" and + * the "new" selection. This makes it impossible + * to tell if an empty cell is a *trailing* empty + * cell (that should not be highlighted), or an + * empty cells between non-empty cells (that + * *should* be highlighted). + * + * Then, when a frame is rendered, we loop the + * *visibible* cells that belong to the + * selection. At this point, we *can* tell if an + * empty cell is trailing or not. + * + * So, what we need to do is check if a + * 'selected', and empty cell has been marked as + * selected, temporarily unmark (forcing it dirty, + * to ensure it gets re-rendered). If it is *not* + * a trailing empty cell, it will get re-tagged as + * selected in the for-loop below. + */ + cell->attrs.clean = false; + cell->attrs.selected = false; + row->dirty = true; + continue; + } + + for (int j = 0; j < empty_count + 1; j++) { + xassert(c - j >= 0); + struct cell *cell = &row->cells[c - j]; + + if (dirty_cells) { + cell->attrs.clean = false; + row->dirty = true; + } + cell->attrs.selected = selected; + } + + empty_count = 0; + } + } + } +} + +static void +selection_modify(struct terminal *term, struct coord start, struct coord end) +{ + xassert(term->selection.coords.start.row != -1); + xassert(start.row != -1 && start.col != -1); + xassert(end.row != -1 && end.col != -1); + + pixman_region32_t previous_selection; + if (term->selection.coords.end.row >= 0) { + previous_selection = pixman_region_for_coords( + term, + &term->selection.coords.start, + &term->selection.coords.end); + } else + pixman_region32_init(&previous_selection); + + pixman_region32_t current_selection = pixman_region_for_coords( + term, &start, &end); + + pixman_region32_t no_longer_selected; + pixman_region32_init(&no_longer_selected); + pixman_region32_subtract( + &no_longer_selected, &previous_selection, ¤t_selection); + + pixman_region32_t newly_selected; + pixman_region32_init(&newly_selected); + pixman_region32_subtract( + &newly_selected, ¤t_selection, &previous_selection); + + /* Clear selection in cells no longer selected */ + int n_rects = -1; + pixman_box32_t *boxes = NULL; + + boxes = pixman_region32_rectangles(&no_longer_selected, &n_rects); + mark_selected_region(term, boxes, n_rects, MARK_SELECTION_UNMARK_AND_DIRTY); + + boxes = pixman_region32_rectangles(&newly_selected, &n_rects); + mark_selected_region(term, boxes, n_rects, MARK_SELECTION_MARK_AND_DIRTY); + + pixman_region32_fini(&newly_selected); + pixman_region32_fini(&no_longer_selected); + pixman_region32_fini(¤t_selection); + pixman_region32_fini(&previous_selection); + + term->selection.coords.start = start; + term->selection.coords.end = end; + render_refresh(term); +} + +static void +set_pivot_point_for_block_and_char_wise(struct terminal *term, + struct coord start, + enum selection_direction new_direction) +{ + struct coord *pivot_start = &term->selection.pivot.start; + struct coord *pivot_end = &term->selection.pivot.end; + + *pivot_start = start; + + /* First, make sure 'start' isn't in the middle of a + * multi-column character */ + while (true) { + const struct row *row = term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)]; + const struct cell *cell = &row->cells[pivot_start->col]; + + if (cell->wc < CELL_SPACER) + break; + + /* Multi-column chars don't cross rows */ + xassert(pivot_start->col > 0); + if (pivot_start->col == 0) + break; + + pivot_start->col--; + } + + /* + * Setup pivot end to be one character *before* start + * Which one we move, the end or start point, depends + * on the initial selection direction. + */ + + *pivot_end = *pivot_start; + + if (new_direction == SELECTION_RIGHT) { + bool keep_going = true; + while (keep_going) { + const struct row *row = term->grid->rows[pivot_end->row & (term->grid->num_rows - 1)]; + const char32_t wc = row->cells[pivot_end->col].wc; + + keep_going = wc >= CELL_SPACER; + + if (pivot_end->col == 0) { + if (pivot_end->row - term->grid->view <= 0) + break; + pivot_end->col = term->cols - 1; + pivot_end->row--; + } else + pivot_end->col--; + } + } else { + bool keep_going = true; + while (keep_going) { + const struct row *row = term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)]; + const char32_t wc = pivot_start->col < term->cols - 1 + ? row->cells[pivot_start->col + 1].wc : 0; + + keep_going = wc >= CELL_SPACER; + + if (pivot_start->col >= term->cols - 1) { + if (pivot_start->row - term->grid->view >= term->rows - 1) + break; + pivot_start->col = 0; + pivot_start->row++; + } else + pivot_start->col++; + } + } + + xassert(term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)]-> + cells[pivot_start->col].wc <= CELL_SPACER); + xassert(term->grid->rows[pivot_end->row & (term->grid->num_rows - 1)]-> + cells[pivot_end->col].wc <= CELL_SPACER + 1); +} + +void +selection_update(struct terminal *term, int col, int row) +{ + if (term->selection.coords.start.row < 0) + return; + + if (!term->selection.ongoing) + return; + + xassert(term->grid->view + row != -1); + + struct coord new_start = term->selection.coords.start; + struct coord new_end = {col, term->grid->view + row}; + + LOG_DBG("selection updated: start = %d,%d, end = %d,%d -> %d, %d", + term->selection.coords.start.row, term->selection.coords.start.col, + term->selection.coords.end.row, term->selection.coords.end.col, + new_end.row, new_end.col); + + /* Adjust start point if the selection has changed 'direction' */ + if (!(new_end.row == new_start.row && new_end.col == new_start.col)) { + enum selection_direction new_direction = term->selection.direction; + + struct coord *pivot_start = &term->selection.pivot.start; + struct coord *pivot_end = &term->selection.pivot.end; + + if (term->selection.kind == SELECTION_BLOCK) { + if (new_end.col > pivot_start->col) + new_direction = SELECTION_RIGHT; + else + new_direction = SELECTION_LEFT; + + if (term->selection.direction == SELECTION_UNDIR) + set_pivot_point_for_block_and_char_wise(term, *pivot_start, new_direction); + + if (new_direction == SELECTION_LEFT) + new_start = *pivot_end; + else + new_start = *pivot_start; + term->selection.direction = new_direction; + } else { + if (new_end.row < pivot_start->row || + (new_end.row == pivot_start->row && + new_end.col < pivot_start->col)) + { + /* New end point is before the start point */ + new_direction = SELECTION_LEFT; + } else { + /* The new end point is after the start point */ + new_direction = SELECTION_RIGHT; + } + + if (term->selection.direction != new_direction) { + if (term->selection.direction == SELECTION_UNDIR && + pivot_end->row < 0) + { + set_pivot_point_for_block_and_char_wise( + term, *pivot_start, new_direction); + } + + if (new_direction == SELECTION_LEFT) { + xassert(pivot_end->row >= 0); + new_start = *pivot_end; + } else + new_start = *pivot_start; + + term->selection.direction = new_direction; + } + } + } + + switch (term->selection.kind) { + case SELECTION_CHAR_WISE: + case SELECTION_BLOCK: + break; + + case SELECTION_WORD_WISE: + switch (term->selection.direction) { + case SELECTION_LEFT: + new_end = (struct coord){col, term->grid->view + row}; + selection_find_word_boundary_left( + term, &new_end, term->selection.spaces_only); + break; + + case SELECTION_RIGHT: + new_end = (struct coord){col, term->grid->view + row}; + selection_find_word_boundary_right( + term, &new_end, term->selection.spaces_only, true); + break; + + case SELECTION_UNDIR: + break; + } + break; + + case SELECTION_QUOTE_WISE: + BUG("quote-wise selection should always be transformed to either word-wise or line-wise"); + break; + + case SELECTION_LINE_WISE: + switch (term->selection.direction) { + case SELECTION_LEFT: { + struct coord end = {0, row}; + selection_find_line_boundary_left(term, &end); + new_end = (struct coord){end.col, term->grid->view + end.row}; + break; + } + + case SELECTION_RIGHT: { + struct coord end = {col, row}; + selection_find_line_boundary_right(term, &end); + new_end = (struct coord){end.col, term->grid->view + end.row}; + break; + } + + case SELECTION_UNDIR: + break; + } + break; + + case SELECTION_NONE: + BUG("Invalid selection kind"); + break; + } + + size_t start_row_idx = new_start.row & (term->grid->num_rows - 1); + size_t end_row_idx = new_end.row & (term->grid->num_rows - 1); + + const struct row *row_start = term->grid->rows[start_row_idx]; + const struct row *row_end = term->grid->rows[end_row_idx]; + + /* If an end point is in the middle of a multi-column character, + * expand the selection to cover the entire character */ + if (new_start.row < new_end.row || + (new_start.row == new_end.row && new_start.col <= new_end.col)) + { + while (new_start.col >= 1 && + row_start->cells[new_start.col].wc >= CELL_SPACER) + new_start.col--; + while (new_end.col < term->cols - 1 && + row_end->cells[new_end.col + 1].wc >= CELL_SPACER) + new_end.col++; + } else { + while (new_end.col >= 1 && + row_end->cells[new_end.col].wc >= CELL_SPACER) + new_end.col--; + while (new_start.col < term->cols - 1 && + row_start->cells[new_start.col + 1].wc >= CELL_SPACER) + new_start.col++; + } + + selection_modify(term, new_start, new_end); +} + +void +selection_dirty_cells(struct terminal *term) +{ + if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0) + return; + + pixman_region32_t selection = pixman_region_for_coords( + term, &term->selection.coords.start, &term->selection.coords.end); + + pixman_region32_t view = pixman_region_for_coords( + term, + &(struct coord){0, term->grid->view}, + &(struct coord){term->cols - 1, term->grid->view + term->rows - 1}); + + pixman_region32_t visible_and_selected; + pixman_region32_init(&visible_and_selected); + pixman_region32_intersect(&visible_and_selected, &selection, &view); + + int n_rects = -1; + pixman_box32_t *boxes = + pixman_region32_rectangles(&visible_and_selected, &n_rects); + mark_selected_region(term, boxes, n_rects, MARK_SELECTION_MARK_FOR_RENDER); + + pixman_region32_fini(&visible_and_selected); + pixman_region32_fini(&view); + pixman_region32_fini(&selection); +} + +static void +selection_extend_normal(struct terminal *term, int col, int row, + enum selection_kind new_kind) +{ + const struct coord *start = &term->selection.coords.start; + const struct coord *end = &term->selection.coords.end; + + const int rel_row = grid_row_abs_to_sb(term->grid, term->rows, row); + int rel_start_row = grid_row_abs_to_sb(term->grid, term->rows, start->row); + int rel_end_row = grid_row_abs_to_sb(term->grid, term->rows, end->row); + + if (rel_start_row > rel_end_row || + (rel_start_row == rel_end_row && start->col > end->col)) + { + const struct coord *tmp = start; + start = end; + end = tmp; + + int tmp_row = rel_start_row; + rel_start_row = rel_end_row; + rel_end_row = tmp_row; + } + + struct coord new_start, new_end; + enum selection_direction direction; + + if (rel_row < rel_start_row || + (rel_row == rel_start_row && col < start->col)) + { + /* Extend selection to start *before* current start */ + new_start = *end; + new_end = (struct coord){col, row}; + direction = SELECTION_LEFT; + } + + else if (rel_row > rel_end_row || + (rel_row == rel_end_row && col > end->col)) + { + /* Extend selection to end *after* current end */ + new_start = *start; + new_end = (struct coord){col, row}; + direction = SELECTION_RIGHT; + } + + else { + /* Shrink selection from start or end, depending on which one is closest */ + + const int linear = rel_row * term->cols + col; + + if (abs(linear - (rel_start_row * term->cols + start->col)) < + abs(linear - (rel_end_row * term->cols + end->col))) + { + /* Move start point */ + new_start = *end; + new_end = (struct coord){col, row}; + direction = SELECTION_LEFT; + } + + else { + /* Move end point */ + new_start = *start; + new_end = (struct coord){col, row}; + direction = SELECTION_RIGHT; + } + } + + const bool spaces_only = term->selection.spaces_only; + + switch (term->selection.kind) { + case SELECTION_CHAR_WISE: + xassert(new_kind == SELECTION_CHAR_WISE); + set_pivot_point_for_block_and_char_wise(term, new_start, direction); + break; + + case SELECTION_WORD_WISE: { + xassert(new_kind == SELECTION_CHAR_WISE || + new_kind == SELECTION_WORD_WISE); + + struct coord pivot_start = {new_start.col, new_start.row}; + struct coord pivot_end = pivot_start; + + selection_find_word_boundary_left(term, &pivot_start, spaces_only); + selection_find_word_boundary_right(term, &pivot_end, spaces_only, true); + + term->selection.pivot.start = pivot_start; + term->selection.pivot.end = pivot_end; + break; + } + + case SELECTION_QUOTE_WISE: { + BUG("quote-wise selection should always be transformed to either word-wise or line-wise"); + break; + } + + case SELECTION_LINE_WISE: { + xassert(new_kind == SELECTION_CHAR_WISE || + new_kind == SELECTION_LINE_WISE); + + struct coord pivot_start = {new_start.col, new_start.row - term->grid->view}; + struct coord pivot_end = pivot_start; + + selection_find_line_boundary_left(term, &pivot_start); + selection_find_line_boundary_right(term, &pivot_end); + + term->selection.pivot.start = + (struct coord){pivot_start.col, term->grid->view + pivot_start.row}; + term->selection.pivot.end = + (struct coord){pivot_end.col, term->grid->view + pivot_end.row}; + break; + } + + case SELECTION_BLOCK: + case SELECTION_NONE: + BUG("Invalid selection kind in this context"); + break; + } + + term->selection.kind = new_kind; + term->selection.direction = direction; + selection_modify(term, new_start, new_end); +} + +static void +selection_extend_block(struct terminal *term, int col, int row) +{ + const struct coord *start = &term->selection.coords.start; + const struct coord *end = &term->selection.coords.end; + + const int rel_start_row = + grid_row_abs_to_sb(term->grid, term->rows, start->row); + const int rel_end_row = + grid_row_abs_to_sb(term->grid, term->rows, end->row); + + struct coord top_left = { + .row = rel_start_row < rel_end_row ? start->row : end->row, + .col = min(start->col, end->col), + }; + + struct coord top_right = { + .row = top_left.row, + .col = max(start->col, end->col), + }; + + struct coord bottom_left = { + .row = rel_start_row > rel_end_row ? start->row : end->row, + .col = min(start->col, end->col), + }; + + struct coord bottom_right = { + .row = bottom_left.row, + .col = max(start->col, end->col), + }; + + const int rel_row = grid_row_abs_to_sb(term->grid, term->rows, row); + const int rel_top_row = grid_row_abs_to_sb(term->grid, term->rows, top_left.row); + const int rel_bottom_row = grid_row_abs_to_sb(term->grid, term->rows, bottom_left.row); + struct coord new_start; + struct coord new_end; + + enum selection_direction direction = SELECTION_UNDIR; + + if (rel_row <= rel_top_row || + abs(rel_row - rel_top_row) < abs(rel_row - rel_bottom_row)) + { + /* Move one of the top corners */ + + if (abs(col - top_left.col) < abs(col - top_right.col)) { + new_start = bottom_right; + new_end = (struct coord){col, row}; + } + + else { + new_start = bottom_left; + new_end = (struct coord){col, row}; + } + } + + else { + /* Move one of the bottom corners */ + + if (abs(col - bottom_left.col) < abs(col - bottom_right.col)) { + new_start = top_right; + new_end = (struct coord){col, row}; + } + + else { + new_start = top_left; + new_end = (struct coord){col, row}; + } + } + + direction = col > new_start.col ? SELECTION_RIGHT : SELECTION_LEFT; + set_pivot_point_for_block_and_char_wise(term, new_start, direction); + + term->selection.direction = direction; + selection_modify(term, new_start, new_end); +} + +void +selection_extend(struct seat *seat, struct terminal *term, + int col, int row, enum selection_kind new_kind) +{ + if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0) { + /* No existing selection */ + return; + } + + if (term->selection.kind == SELECTION_BLOCK && new_kind != SELECTION_BLOCK) + return; + + term->selection.ongoing = true; + + row += term->grid->view; + + if ((row == term->selection.coords.start.row && col == term->selection.coords.start.col) || + (row == term->selection.coords.end.row && col == term->selection.coords.end.col)) + { + /* Extension point *is* one of the current end points */ + return; + } + + switch (term->selection.kind) { + case SELECTION_NONE: + BUG("Invalid selection kind"); + return; + + case SELECTION_CHAR_WISE: + case SELECTION_WORD_WISE: + case SELECTION_QUOTE_WISE: + case SELECTION_LINE_WISE: + selection_extend_normal(term, col, row, new_kind); + break; + + case SELECTION_BLOCK: + selection_extend_block(term, col, row); + break; + } +} + +//static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener; + +void +selection_finalize(struct seat *seat, struct terminal *term, uint32_t serial) +{ + if (!term->selection.ongoing) + return; + + LOG_DBG("selection finalize"); + + selection_stop_scroll_timer(term); + term->selection.ongoing = false; + + if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0) + return; + + xassert(term->selection.coords.start.row != -1); + xassert(term->selection.coords.end.row != -1); + + term->selection.coords.start.row &= (term->grid->num_rows - 1); + term->selection.coords.end.row &= (term->grid->num_rows - 1); + + switch (term->conf->selection_target) { + case SELECTION_TARGET_NONE: + break; + + case SELECTION_TARGET_PRIMARY: + selection_to_primary(seat, term, serial); + break; + case SELECTION_TARGET_CLIPBOARD: + selection_to_clipboard(seat, term, serial); + break; + + case SELECTION_TARGET_BOTH: + selection_to_primary(seat, term, serial); + selection_to_clipboard(seat, term, serial); + break; + } +} + +static bool +unmark_selected(struct terminal *term, struct row *row, struct cell *cell, + int row_no, int col, void *data) +{ + if (!cell->attrs.selected) + return true; + + row->dirty = true; + cell->attrs.selected = false; + cell->attrs.clean = false; + return true; +} + +void +selection_cancel(struct terminal *term) +{ + LOG_DBG("selection cancelled: start = %d,%d end = %d,%d", + term->selection.coords.start.row, term->selection.coords.start.col, + term->selection.coords.end.row, term->selection.coords.end.col); + + selection_stop_scroll_timer(term); + + if (term->selection.coords.start.row >= 0 && term->selection.coords.end.row >= 0) { + foreach_selected( + term, term->selection.coords.start, term->selection.coords.end, + &unmark_selected, NULL); + render_refresh(term); + } + + term->selection.kind = SELECTION_NONE; + term->selection.coords.start = (struct coord){-1, -1}; + term->selection.coords.end = (struct coord){-1, -1}; + term->selection.pivot.start = (struct coord){-1, -1}; + term->selection.pivot.end = (struct coord){-1, -1}; + term->selection.direction = SELECTION_UNDIR; + term->selection.ongoing = false; + + search_selection_cancelled(term); +} + +bool +selection_clipboard_has_data(const struct seat *seat) +{ + return seat->clipboard.data_offer != NULL; +} + +bool +selection_primary_has_data(const struct seat *seat) +{ + return seat->primary.data_offer != NULL; +} + +void +selection_clipboard_unset(struct seat *seat) +{ + struct wl_clipboard *clipboard = &seat->clipboard; + + if (clipboard->data_source == NULL) + return; + + /* Kill previous data source */ + xassert(clipboard->serial != 0); + wl_data_device_set_selection(seat->data_device, NULL, clipboard->serial); + wl_data_source_destroy(clipboard->data_source); + + clipboard->data_source = NULL; + clipboard->serial = 0; + + free(clipboard->text); + clipboard->text = NULL; +} + +void +selection_primary_unset(struct seat *seat) +{ + struct wl_primary *primary = &seat->primary; + + if (primary->data_source == NULL) + return; + + xassert(primary->serial != 0); + zwp_primary_selection_device_v1_set_selection( + seat->primary_selection_device, NULL, primary->serial); + zwp_primary_selection_source_v1_destroy(primary->data_source); + + primary->data_source = NULL; + primary->serial = 0; + + free(primary->text); + primary->text = NULL; +} + +static bool +fdm_scroll_timer(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + + uint64_t expiration_count; + ssize_t ret = read( + term->selection.auto_scroll.fd, + &expiration_count, sizeof(expiration_count)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read selection scroll timer"); + return false; + } + + switch (term->selection.auto_scroll.direction) { + case SELECTION_SCROLL_NOT: + return true; + + case SELECTION_SCROLL_UP: + cmd_scrollback_up(term, expiration_count); + selection_update(term, term->selection.auto_scroll.col, 0); + break; + + case SELECTION_SCROLL_DOWN: + cmd_scrollback_down(term, expiration_count); + selection_update(term, term->selection.auto_scroll.col, term->rows - 1); + break; + } + + + return true; +} + +void +selection_start_scroll_timer(struct terminal *term, int interval_ns, + enum selection_scroll_direction direction, int col) +{ + xassert(direction != SELECTION_SCROLL_NOT); + + if (!term->selection.ongoing) + return; + + if (term->selection.auto_scroll.fd < 0) { + int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd < 0) { + LOG_ERRNO("failed to create selection scroll timer"); + goto err; + } + + if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_scroll_timer, term)) { + close(fd); + return; + } + + term->selection.auto_scroll.fd = fd; + } + + struct itimerspec timer; + if (timerfd_gettime(term->selection.auto_scroll.fd, &timer) < 0) { + LOG_ERRNO("failed to get current selection scroll timer value"); + goto err; + } + + if (timer.it_value.tv_sec == 0 && timer.it_value.tv_nsec == 0) + timer.it_value.tv_nsec = 1; + + timer.it_interval.tv_sec = interval_ns / 1000000000; + timer.it_interval.tv_nsec = interval_ns % 1000000000; + + if (timerfd_settime(term->selection.auto_scroll.fd, 0, &timer, NULL) < 0) { + LOG_ERRNO("failed to set new selection scroll timer value"); + goto err; + } + + term->selection.auto_scroll.direction = direction; + term->selection.auto_scroll.col = col; + return; + +err: + selection_stop_scroll_timer(term); + return; +} + +void +selection_stop_scroll_timer(struct terminal *term) +{ + if (term->selection.auto_scroll.fd < 0) { + xassert(term->selection.auto_scroll.direction == SELECTION_SCROLL_NOT); + return; + } + + fdm_del(term->fdm, term->selection.auto_scroll.fd); + term->selection.auto_scroll.fd = -1; + term->selection.auto_scroll.direction = SELECTION_SCROLL_NOT; +} + +static void +target(void *data, struct wl_data_source *wl_data_source, const char *mime_type) +{ + LOG_DBG("TARGET: mime-type=%s", mime_type); +} + +struct clipboard_send { + char *data; + size_t len; + size_t idx; +}; + +static bool +fdm_send(struct fdm *fdm, int fd, int events, void *data) +{ + struct clipboard_send *ctx = data; + + if (events & EPOLLHUP) + goto done; + + switch (async_write(fd, ctx->data, ctx->len, &ctx->idx)) { + case ASYNC_WRITE_REMAIN: + return true; + + case ASYNC_WRITE_DONE: + break; + + case ASYNC_WRITE_ERR: + LOG_ERRNO( + "failed to asynchronously write %zu of selection data to FD=%d", + ctx->len - ctx->idx, fd); + break; + } + +done: + fdm_del(fdm, fd); + free(ctx->data); + free(ctx); + return true; +} + +static void +send_clipboard_or_primary(struct seat *seat, int fd, const char *selection, + const char *source_name) +{ + /* Make it NONBLOCK:ing right away - we don't want to block if the + * initial attempt to send the data synchronously fails */ + int flags; + if ((flags = fcntl(fd, F_GETFL)) < 0 || + fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) + { + LOG_ERRNO("failed to set O_NONBLOCK"); + return; + } + + size_t len = selection != NULL ? strlen(selection) : 0; + size_t async_idx = 0; + + switch (async_write(fd, selection, len, &async_idx)) { + case ASYNC_WRITE_REMAIN: { + struct clipboard_send *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct clipboard_send) { + .data = xstrdup(&selection[async_idx]), + .len = len - async_idx, + .idx = 0, + }; + + if (fdm_add(seat->wayl->fdm, fd, EPOLLOUT, &fdm_send, ctx)) + return; + + free(ctx->data); + free(ctx); + break; + } + + case ASYNC_WRITE_DONE: + break; + + case ASYNC_WRITE_ERR: + LOG_ERRNO("failed write %zu bytes of %s selection data to FD=%d", + len, source_name, fd); + break; + } + + close(fd); +} + +static void +send(void *data, struct wl_data_source *wl_data_source, const char *mime_type, + int32_t fd) +{ + struct seat *seat = data; + const struct wl_clipboard *clipboard = &seat->clipboard; + + send_clipboard_or_primary(seat, fd, clipboard->text, "clipboard"); +} + +static void +cancelled(void *data, struct wl_data_source *wl_data_source) +{ + struct seat *seat = data; + struct wl_clipboard *clipboard = &seat->clipboard; + xassert(clipboard->data_source == wl_data_source); + + wl_data_source_destroy(clipboard->data_source); + clipboard->data_source = NULL; + clipboard->serial = 0; + + free(clipboard->text); + clipboard->text = NULL; +} + +/* We don't support dragging *from* */ +static void +dnd_drop_performed(void *data, struct wl_data_source *wl_data_source) +{ + //LOG_DBG("DnD drop performed"); +} + +static void +dnd_finished(void *data, struct wl_data_source *wl_data_source) +{ + //LOG_DBG("DnD finished"); +} + +static void +action(void *data, struct wl_data_source *wl_data_source, uint32_t dnd_action) +{ + //LOG_DBG("DnD action: %u", dnd_action); +} + +static const struct wl_data_source_listener data_source_listener = { + .target = &target, + .send = &send, + .cancelled = &cancelled, + .dnd_drop_performed = &dnd_drop_performed, + .dnd_finished = &dnd_finished, + .action = &action, +}; + +static void +primary_send(void *data, + struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1, + const char *mime_type, int32_t fd) +{ + struct seat *seat = data; + const struct wl_primary *primary = &seat->primary; + + send_clipboard_or_primary(seat, fd, primary->text, "primary"); +} + +static void +primary_cancelled(void *data, + struct zwp_primary_selection_source_v1 *zwp_primary_selection_source_v1) +{ + struct seat *seat = data; + struct wl_primary *primary = &seat->primary; + + zwp_primary_selection_source_v1_destroy(primary->data_source); + primary->data_source = NULL; + primary->serial = 0; + + free(primary->text); + primary->text = NULL; +} + +static const struct zwp_primary_selection_source_v1_listener primary_selection_source_listener = { + .send = &primary_send, + .cancelled = &primary_cancelled, +}; + +bool +text_to_clipboard(struct seat *seat, struct terminal *term, char *text, uint32_t serial) +{ + xassert(serial != 0); + + struct wl_clipboard *clipboard = &seat->clipboard; + + if (clipboard->data_source != NULL) { + /* Kill previous data source */ + xassert(clipboard->serial != 0); + wl_data_device_set_selection(seat->data_device, NULL, clipboard->serial); + wl_data_source_destroy(clipboard->data_source); + free(clipboard->text); + + clipboard->data_source = NULL; + clipboard->serial = 0; + clipboard->text = NULL; + } + + clipboard->data_source + = wl_data_device_manager_create_data_source(term->wl->data_device_manager); + + if (clipboard->data_source == NULL) { + LOG_ERR("failed to create clipboard data source"); + return false; + } + + clipboard->text = text; + + /* Configure source */ + wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8]); + wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_PLAIN]); + wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_TEXT]);; + wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_STRING]); + wl_data_source_offer(clipboard->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8_STRING]); + + wl_data_source_add_listener(clipboard->data_source, &data_source_listener, seat); + wl_data_device_set_selection(seat->data_device, clipboard->data_source, serial); + + /* Needed when sending the selection to other client */ + clipboard->serial = serial; + return true; +} + +void +selection_to_clipboard(struct seat *seat, struct terminal *term, uint32_t serial) +{ + if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0) + return; + + /* Get selection as a string */ + char *text = selection_to_text(term); + if (!text_to_clipboard(seat, term, text, serial)) + free(text); +} + +struct clipboard_receive { + int read_fd; + int timeout_fd; + struct itimerspec timeout; + bool bracketed; + bool no_strip; + bool quote_paths; + + void (*decoder)(struct clipboard_receive *ctx, char *data, size_t size); + void (*finish)(struct clipboard_receive *ctx); + + /* URI state */ + bool add_space; + struct { + char *data; + size_t sz; + size_t idx; + } buf; + + /* Callback data */ + void (*cb)(char *data, size_t size, void *user); + void (*done)(void *user); + void *user; +}; + +static void +clipboard_receive_done(struct fdm *fdm, struct clipboard_receive *ctx) +{ + fdm_del(fdm, ctx->timeout_fd); + fdm_del(fdm, ctx->read_fd); + ctx->done(ctx->user); + free(ctx->buf.data); + free(ctx); +} + +static bool +fdm_receive_timeout(struct fdm *fdm, int fd, int events, void *data) +{ + struct clipboard_receive *ctx = data; + if (events & EPOLLHUP) + return false; + + xassert(events & EPOLLIN); + + uint64_t expire_count; + ssize_t ret = read(fd, &expire_count, sizeof(expire_count)); + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read clipboard timeout timer"); + return false; + } + + LOG_WARN("no data received from clipboard in %llu seconds, aborting", + (unsigned long long)ctx->timeout.it_value.tv_sec); + + clipboard_receive_done(fdm, ctx); + return true; +} + +static void +fdm_receive_decoder_plain(struct clipboard_receive *ctx, char *data, size_t size) +{ + ctx->cb(data, size, ctx->user); +} + +static void +fdm_receive_finish_plain(struct clipboard_receive *ctx) +{ +} + +static bool +decode_one_uri(struct clipboard_receive *ctx, char *uri, size_t len) +{ + LOG_DBG("URI: \"%.*s\"", (int)len, uri); + + if (len == 0) + return false; + + char *scheme, *host, *path; + if (!uri_parse(uri, len, &scheme, NULL, NULL, &host, NULL, &path, NULL, NULL)) { + LOG_ERR("drag-and-drop: invalid URI: %.*s", (int)len, uri); + return false; + } + + if (ctx->add_space) + ctx->cb(" ", 1, ctx->user); + ctx->add_space = true; + + if (streq(scheme, "file") && hostname_is_localhost(host)) { + if (ctx->quote_paths) + ctx->cb("'", 1, ctx->user); + + ctx->cb(path, strlen(path), ctx->user); + + if (ctx->quote_paths) + ctx->cb("'", 1, ctx->user); + } else + ctx->cb(uri, len, ctx->user); + + free(scheme); + free(host); + free(path); + return true; +} + +static void +fdm_receive_decoder_uri(struct clipboard_receive *ctx, char *data, size_t size) +{ + while (ctx->buf.idx + size > ctx->buf.sz) { + size_t new_sz = ctx->buf.sz == 0 ? size : 2 * ctx->buf.sz; + ctx->buf.data = xrealloc(ctx->buf.data, new_sz); + ctx->buf.sz = new_sz; + } + + memcpy(&ctx->buf.data[ctx->buf.idx], data, size); + ctx->buf.idx += size; + + char *start = ctx->buf.data; + char *end = NULL; + + while (true) { + for (end = start; end < &ctx->buf.data[ctx->buf.idx]; end++) { + if (*end == '\r' || *end == '\n') + break; + } + + if (end >= &ctx->buf.data[ctx->buf.idx]) + break; + + decode_one_uri(ctx, start, end - start); + start = end + 1; + } + + const size_t ofs = start - ctx->buf.data; + const size_t left = ctx->buf.idx - ofs; + + memmove(&ctx->buf.data[0], &ctx->buf.data[ofs], left); + ctx->buf.idx = left; +} + +static void +fdm_receive_finish_uri(struct clipboard_receive *ctx) +{ + LOG_DBG("finish: %.*s", (int)ctx->buf.idx, ctx->buf.data); + decode_one_uri(ctx, ctx->buf.data, ctx->buf.idx); +} + +static bool +fdm_receive(struct fdm *fdm, int fd, int events, void *data) +{ + struct clipboard_receive *ctx = data; + const bool no_strip = ctx->no_strip; + const bool bracketed = ctx->bracketed; + + if ((events & EPOLLHUP) && !(events & EPOLLIN)) + goto done; + + /* Reset timeout timer */ + if (timerfd_settime(ctx->timeout_fd, 0, &ctx->timeout, NULL) < 0) { + LOG_ERRNO("failed to re-arm clipboard timeout timer"); + return false; + } + + /* Read until EOF */ + while (true) { + char text[256]; + ssize_t count = read(fd, text, sizeof(text)); + + if (count == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + return true; + + LOG_ERRNO("failed to read clipboard data"); + break; + } + + if (count == 0) + break; + + /* + * Call cb while at same time replace: + * - \r\n -> \r (non-bracketed paste) + * - \n -> \r (non-bracketed paste) + * - C0 -> (strip non-formatting C0 characters) + * - \e -> (i.e. strip ESC) + */ + char *p = text; + size_t left = count; + +#define skip_one() \ + do { \ + ctx->decoder(ctx, p, i); \ + xassert(i + 1 <= left); \ + p += i + 1; \ + left -= i + 1; \ + } while (0) + + again: + for (size_t i = 0; i < left; i++) { + switch (p[i]) { + default: + break; + + case '\n': + if (!no_strip && !bracketed) { + p[i] = '\r'; + } + break; + + case '\r': + /* Convert \r\n -> \r */ + if (!no_strip && !bracketed && i + 1 < left && p[i + 1] == '\n') { + i++; + skip_one(); + goto again; + } + break; + + /* C0 non-formatting control characters (\b \t \n \r excluded) */ + case '\x01': case '\x02': case '\x03': case '\x04': case '\x05': + case '\x06': case '\x07': case '\x0e': case '\x0f': case '\x10': + case '\x11': case '\x12': case '\x13': case '\x14': case '\x15': + case '\x16': case '\x17': case '\x18': case '\x19': case '\x1a': + case '\x1b': case '\x1c': case '\x1d': case '\x1e': case '\x1f': + if (!no_strip) { + skip_one(); + goto again; + } + break; + + /* + * In addition to stripping non-formatting C0 controls, + * XTerm has an option, "disallowedPasteControls", that + * defines C0 controls that will be replaced with spaces + * when pasted. + * + * It's default value is BS,DEL,ENQ,EOT,NUL + * + * Instead of replacing them with spaces, we allow them in + * bracketed paste mode, and strip them completely in + * non-bracketed mode. + * + * Note some of the (default) XTerm controls are already + * handled above. + */ + case '\b': case '\x7f': case '\x00': + if (!no_strip && !bracketed) { + skip_one(); + goto again; + } + break; + } + } + + ctx->decoder(ctx, p, left); + left = 0; + } + +#undef skip_one + +done: + ctx->finish(ctx); + clipboard_receive_done(fdm, ctx); + return true; +} + +static void +begin_receive_clipboard(struct terminal *term, bool no_strip, + int read_fd, enum data_offer_mime_type mime_type, + void (*cb)(char *data, size_t size, void *user), + void (*done)(void *user), void *user) +{ + int timeout_fd = -1; + struct clipboard_receive *ctx = NULL; + + int flags; + if ((flags = fcntl(read_fd, F_GETFL)) < 0 || + fcntl(read_fd, F_SETFL, flags | O_NONBLOCK) < 0) + { + LOG_ERRNO("failed to set O_NONBLOCK"); + goto err; + } + + timeout_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); + if (timeout_fd < 0) { + LOG_ERRNO("failed to create clipboard timeout timer FD"); + goto err; + } + + const struct itimerspec timeout = {.it_value = {.tv_sec = 2}}; + if (timerfd_settime(timeout_fd, 0, &timeout, NULL) < 0) { + LOG_ERRNO("failed to arm clipboard timeout timer"); + goto err; + } + + ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct clipboard_receive) { + .read_fd = read_fd, + .timeout_fd = timeout_fd, + .timeout = timeout, + .bracketed = term->bracketed_paste, + .no_strip = no_strip, + .quote_paths = term->grid == &term->normal, + .decoder = (mime_type == DATA_OFFER_MIME_URI_LIST + ? &fdm_receive_decoder_uri + : &fdm_receive_decoder_plain), + .finish = (mime_type == DATA_OFFER_MIME_URI_LIST + ? &fdm_receive_finish_uri + : &fdm_receive_finish_plain), + .cb = cb, + .done = done, + .user = user, + }; + + if (!fdm_add(term->fdm, read_fd, EPOLLIN, &fdm_receive, ctx) || + !fdm_add(term->fdm, timeout_fd, EPOLLIN, &fdm_receive_timeout, ctx)) + { + goto err; + } + + return; + +err: + free(ctx); + fdm_del(term->fdm, timeout_fd); + fdm_del(term->fdm, read_fd); + done(user); +} + +void +text_from_clipboard(struct seat *seat, struct terminal *term, + bool no_strip, + void (*cb)(char *data, size_t size, void *user), + void (*done)(void *user), void *user) +{ + struct wl_clipboard *clipboard = &seat->clipboard; + if (clipboard->data_offer == NULL || + clipboard->mime_type == DATA_OFFER_MIME_UNSET) + { + done(user); + return; + } + + /* Prepare a pipe the other client can write its selection to us */ + int fds[2]; + if (pipe2(fds, O_CLOEXEC) == -1) { + LOG_ERRNO("failed to create pipe"); + done(user); + return; + } + + LOG_DBG("receive from clipboard: mime-type=%s", + mime_type_map[clipboard->mime_type]); + + int read_fd = fds[0]; + int write_fd = fds[1]; + + /* Give write-end of pipe to other client */ + wl_data_offer_receive( + clipboard->data_offer, mime_type_map[clipboard->mime_type], write_fd); + + /* Don't keep our copy of the write-end open (or we'll never get EOF) */ + close(write_fd); + + begin_receive_clipboard( + term, no_strip, read_fd, clipboard->mime_type, cb, done, user); +} + +static void +receive_offer(char *data, size_t size, void *user) +{ + struct terminal *term = user; + xassert(term->is_sending_paste_data); + term_paste_data_to_slave(term, data, size); +} + +static void +receive_offer_done(void *user) +{ + struct terminal *term = user; + + if (term->bracketed_paste) + term_paste_data_to_slave(term, "\033[201~", 6); + + term->is_sending_paste_data = false; + + /* Make sure we send any queued up non-paste data */ + if (tll_length(term->ptmx_buffers) > 0) + fdm_event_add(term->fdm, term->ptmx, EPOLLOUT); +} + +void +selection_from_clipboard(struct seat *seat, struct terminal *term, uint32_t serial) +{ + if (term->is_sending_paste_data) { + /* We're already pasting... */ + return; + } + + struct wl_clipboard *clipboard = &seat->clipboard; + if (clipboard->data_offer == NULL) + return; + + term->is_sending_paste_data = true; + + if (term->bracketed_paste) + term_paste_data_to_slave(term, "\033[200~", 6); + + text_from_clipboard( + seat, term, false, &receive_offer, &receive_offer_done, term); +} + +bool +text_to_primary(struct seat *seat, struct terminal *term, char *text, uint32_t serial) +{ + if (term->wl->primary_selection_device_manager == NULL) + return false; + + xassert(serial != 0); + + struct wl_primary *primary = &seat->primary; + + /* TODO: somehow share code with the clipboard equivalent */ + if (seat->primary.data_source != NULL) { + /* Kill previous data source */ + + xassert(primary->serial != 0); + zwp_primary_selection_device_v1_set_selection( + seat->primary_selection_device, NULL, primary->serial); + zwp_primary_selection_source_v1_destroy(primary->data_source); + free(primary->text); + + primary->data_source = NULL; + primary->serial = 0; + primary->text = NULL; + } + + primary->data_source + = zwp_primary_selection_device_manager_v1_create_source( + term->wl->primary_selection_device_manager); + + if (primary->data_source == NULL) { + LOG_ERR("failed to create clipboard data source"); + return false; + } + + /* Get selection as a string */ + primary->text = text; + + /* Configure source */ + zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8]); + zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_PLAIN]); + zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_TEXT]); + zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_STRING]); + zwp_primary_selection_source_v1_offer(primary->data_source, mime_type_map[DATA_OFFER_MIME_TEXT_UTF8_STRING]); + + zwp_primary_selection_source_v1_add_listener(primary->data_source, &primary_selection_source_listener, seat); + zwp_primary_selection_device_v1_set_selection(seat->primary_selection_device, primary->data_source, serial); + + /* Needed when sending the selection to other client */ + primary->serial = serial; + return true; +} + +void +selection_to_primary(struct seat *seat, struct terminal *term, uint32_t serial) +{ + if (term->wl->primary_selection_device_manager == NULL) + return; + + /* Get selection as a string */ + char *text = selection_to_text(term); + if (!text_to_primary(seat, term, text, serial)) + free(text); +} + +void +text_from_primary( + struct seat *seat, struct terminal *term, bool no_strip, + void (*cb)(char *data, size_t size, void *user), + void (*done)(void *user), void *user) +{ + if (term->wl->primary_selection_device_manager == NULL) { + done(user); + return; + } + + struct wl_primary *primary = &seat->primary; + if (primary->data_offer == NULL || + primary->mime_type == DATA_OFFER_MIME_UNSET) + { + done(user); + return; + } + + /* Prepare a pipe the other client can write its selection to us */ + int fds[2]; + if (pipe2(fds, O_CLOEXEC) == -1) { + LOG_ERRNO("failed to create pipe"); + done(user); + return; + } + + LOG_DBG("receive from primary: mime-type=%s", + mime_type_map[primary->mime_type]); + + int read_fd = fds[0]; + int write_fd = fds[1]; + + /* Give write-end of pipe to other client */ + zwp_primary_selection_offer_v1_receive( + primary->data_offer, mime_type_map[primary->mime_type], write_fd); + + /* Don't keep our copy of the write-end open (or we'll never get EOF) */ + close(write_fd); + + begin_receive_clipboard( + term, no_strip, read_fd, primary->mime_type, cb, done, user); +} + +void +selection_from_primary(struct seat *seat, struct terminal *term) +{ + if (term->wl->primary_selection_device_manager == NULL) + return; + + if (term->is_sending_paste_data) { + /* We're already pasting... */ + return; + } + + struct wl_primary *primary = &seat->primary; + if (primary->data_offer == NULL) + return; + + term->is_sending_paste_data = true; + if (term->bracketed_paste) + term_paste_data_to_slave(term, "\033[200~", 6); + + text_from_primary( + seat, term, false, &receive_offer, &receive_offer_done, term); +} + +static void +select_mime_type_for_offer(const char *_mime_type, + enum data_offer_mime_type *type) +{ + enum data_offer_mime_type mime_type = DATA_OFFER_MIME_UNSET; + + /* Translate offered mime type to our mime type enum */ + for (size_t i = 0; i < ALEN(mime_type_map); i++) { + if (mime_type_map[i] == NULL) + continue; + + if (streq(_mime_type, mime_type_map[i])) { + mime_type = i; + break; + } + } + + LOG_DBG("mime-type: %s -> %s (offered type was %s)", + mime_type_map[*type], mime_type_map[mime_type], _mime_type); + + /* Mime-type transition; if the new mime-type is "better" than + * previously offered types, use the new type */ + + switch (mime_type) { + case DATA_OFFER_MIME_TEXT_PLAIN: + case DATA_OFFER_MIME_TEXT_TEXT: + case DATA_OFFER_MIME_TEXT_STRING: + /* text/plain is our least preferred type. Only use if current + * type is unset */ + switch (*type) { + case DATA_OFFER_MIME_UNSET: + *type = mime_type; + break; + + default: + break; + } + break; + + case DATA_OFFER_MIME_TEXT_UTF8: + case DATA_OFFER_MIME_TEXT_UTF8_STRING: + /* text/plain;charset=utf-8 is preferred over text/plain */ + switch (*type) { + case DATA_OFFER_MIME_UNSET: + case DATA_OFFER_MIME_TEXT_PLAIN: + case DATA_OFFER_MIME_TEXT_TEXT: + case DATA_OFFER_MIME_TEXT_STRING: + *type = mime_type; + break; + + default: + break; + } + break; + + case DATA_OFFER_MIME_URI_LIST: + /* text/uri-list is always used when offered */ + *type = mime_type; + break; + + case DATA_OFFER_MIME_UNSET: + break; + } +} + +static void +data_offer_reset(struct wl_clipboard *clipboard) +{ + if (clipboard->data_offer != NULL) { + wl_data_offer_destroy(clipboard->data_offer); + clipboard->data_offer = NULL; + } + + clipboard->window = NULL; + clipboard->mime_type = DATA_OFFER_MIME_UNSET; +} + +static void +offer(void *data, struct wl_data_offer *wl_data_offer, const char *mime_type) +{ + struct seat *seat = data; + select_mime_type_for_offer(mime_type, &seat->clipboard.mime_type); +} + +static void +source_actions(void *data, struct wl_data_offer *wl_data_offer, + uint32_t source_actions) +{ +#if defined(_DEBUG) && LOG_ENABLE_DBG + char actions_as_string[1024]; + size_t idx = 0; + + actions_as_string[0] = '\0'; + actions_as_string[sizeof(actions_as_string) - 1] = '\0'; + + for (size_t i = 0; i < 31; i++) { + if (((source_actions >> i) & 1) == 0) + continue; + + enum wl_data_device_manager_dnd_action action = 1 << i; + + const char *s = NULL; + + switch (action) { + case WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE: s = NULL; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY: s = "copy"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE: s = "move"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK: s = "ask"; break; + } + + if (s == NULL) + continue; + + strncat(actions_as_string, s, sizeof(actions_as_string) - idx - 1); + idx += strlen(s); + strncat(actions_as_string, ", ", sizeof(actions_as_string) - idx - 1); + idx += 2; + } + + /* Strip trailing ", " */ + if (strlen(actions_as_string) > 2) + actions_as_string[strlen(actions_as_string) - 2] = '\0'; + + LOG_DBG("DnD actions: %s (0x%08x)", actions_as_string, source_actions); +#endif +} + +static void +offer_action(void *data, struct wl_data_offer *wl_data_offer, uint32_t dnd_action) +{ +#if defined(_DEBUG) && LOG_ENABLE_DBG + const char *s = NULL; + + switch (dnd_action) { + case WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE: s = ""; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY: s = "copy"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE: s = "move"; break; + case WL_DATA_DEVICE_MANAGER_DND_ACTION_ASK: s = "ask"; break; + } + + LOG_DBG("DnD offer action: %s (0x%08x)", s, dnd_action); +#endif +} + +static const struct wl_data_offer_listener data_offer_listener = { + .offer = &offer, + .source_actions = &source_actions, + .action = &offer_action, +}; + +static void +data_offer(void *data, struct wl_data_device *wl_data_device, + struct wl_data_offer *offer) +{ + struct seat *seat = data; + data_offer_reset(&seat->clipboard); + seat->clipboard.data_offer = offer; + wl_data_offer_add_listener(offer, &data_offer_listener, seat); +} + +static void +enter(void *data, struct wl_data_device *wl_data_device, uint32_t serial, + struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y, + struct wl_data_offer *offer) +{ + struct seat *seat = data; + struct wayland *wayl = seat->wayl; + + xassert(offer == seat->clipboard.data_offer); + + if (seat->clipboard.mime_type == DATA_OFFER_MIME_UNSET) + goto reject_offer; + + /* Remember _which_ terminal the current DnD offer is targeting */ + xassert(seat->clipboard.window == NULL); + tll_foreach(wayl->terms, it) { + if (term_surface_kind(it->item, surface) == TERM_SURF_GRID && + !it->item->is_sending_paste_data) + { + wl_data_offer_accept( + offer, serial, mime_type_map[seat->clipboard.mime_type]); + wl_data_offer_set_actions( + offer, + WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY, + WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY); + + seat->clipboard.window = it->item->window; + return; + } + } + +reject_offer: + /* Either terminal is already busy sending paste data, or mouse + * pointer isn't over the grid */ + seat->clipboard.window = NULL; + wl_data_offer_accept(offer, serial, NULL); + wl_data_offer_set_actions( + offer, + WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE, + WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE); +} + +static void +leave(void *data, struct wl_data_device *wl_data_device) +{ + struct seat *seat = data; + seat->clipboard.window = NULL; +} + +static void +motion(void *data, struct wl_data_device *wl_data_device, uint32_t time, + wl_fixed_t x, wl_fixed_t y) +{ +} + +struct dnd_context { + struct terminal *term; + struct wl_data_offer *data_offer; +}; + +static void +receive_dnd(char *data, size_t size, void *user) +{ + struct dnd_context *ctx = user; + receive_offer(data, size, ctx->term); +} + +static void +receive_dnd_done(void *user) +{ + struct dnd_context *ctx = user; + + wl_data_offer_finish(ctx->data_offer); + wl_data_offer_destroy(ctx->data_offer); + receive_offer_done(ctx->term); + free(ctx); +} + +static void +drop(void *data, struct wl_data_device *wl_data_device) +{ + struct seat *seat = data; + + xassert(seat->clipboard.window != NULL); + struct terminal *term = seat->clipboard.window->term; + + struct wl_clipboard *clipboard = &seat->clipboard; + + if (clipboard->mime_type == DATA_OFFER_MIME_UNSET) { + LOG_WARN("compositor called data_device::drop() " + "even though we rejected the drag-and-drop"); + return; + } + + struct dnd_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct dnd_context){ + .term = term, + .data_offer = clipboard->data_offer, + }; + + /* Prepare a pipe the other client can write its selection to us */ + int fds[2]; + if (pipe2(fds, O_CLOEXEC) == -1) { + LOG_ERRNO("failed to create pipe"); + free(ctx); + return; + } + + int read_fd = fds[0]; + int write_fd = fds[1]; + + LOG_DBG("DnD drop: mime-type=%s", mime_type_map[clipboard->mime_type]); + + /* Give write-end of pipe to other client */ + wl_data_offer_receive( + clipboard->data_offer, mime_type_map[clipboard->mime_type], write_fd); + + /* Don't keep our copy of the write-end open (or we'll never get EOF) */ + close(write_fd); + + term->is_sending_paste_data = true; + + if (term->bracketed_paste) + term_paste_data_to_slave(term, "\033[200~", 6); + + begin_receive_clipboard( + term, false, read_fd, clipboard->mime_type, + &receive_dnd, &receive_dnd_done, ctx); + + /* data offer is now "owned" by the receive context */ + clipboard->data_offer = NULL; + clipboard->mime_type = DATA_OFFER_MIME_UNSET; +} + +static void +selection(void *data, struct wl_data_device *wl_data_device, + struct wl_data_offer *offer) +{ + /* Selection offer from other client */ + struct seat *seat = data; + if (offer == NULL) + data_offer_reset(&seat->clipboard); + else + xassert(offer == seat->clipboard.data_offer); +} + +const struct wl_data_device_listener data_device_listener = { + .data_offer = &data_offer, + .enter = &enter, + .leave = &leave, + .motion = &motion, + .drop = &drop, + .selection = &selection, +}; + +static void +primary_offer(void *data, + struct zwp_primary_selection_offer_v1 *zwp_primary_selection_offer, + const char *mime_type) +{ + LOG_DBG("primary offer: %s", mime_type); + struct seat *seat = data; + select_mime_type_for_offer(mime_type, &seat->primary.mime_type); +} + +static const struct zwp_primary_selection_offer_v1_listener primary_selection_offer_listener = { + .offer = &primary_offer, +}; + +static void +primary_offer_reset(struct wl_primary *primary) +{ + if (primary->data_offer != NULL) { + zwp_primary_selection_offer_v1_destroy(primary->data_offer); + primary->data_offer = NULL; + } + + primary->mime_type = DATA_OFFER_MIME_UNSET; +} + +static void +primary_data_offer(void *data, + struct zwp_primary_selection_device_v1 *zwp_primary_selection_device, + struct zwp_primary_selection_offer_v1 *offer) +{ + struct seat *seat = data; + primary_offer_reset(&seat->primary); + seat->primary.data_offer = offer; + zwp_primary_selection_offer_v1_add_listener( + offer, &primary_selection_offer_listener, seat); +} + +static void +primary_selection(void *data, + struct zwp_primary_selection_device_v1 *zwp_primary_selection_device, + struct zwp_primary_selection_offer_v1 *offer) +{ + /* Selection offer from other client, for primary */ + + struct seat *seat = data; + if (offer == NULL) + primary_offer_reset(&seat->primary); + else + xassert(seat->primary.data_offer == offer); +} + +const struct zwp_primary_selection_device_v1_listener primary_selection_device_listener = { + .data_offer = &primary_data_offer, + .selection = &primary_selection, +}; + diff --git a/selection.h b/selection.h new file mode 100644 index 0000000..b6ad099 --- /dev/null +++ b/selection.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include + +#include "terminal.h" + +extern const struct wl_data_device_listener data_device_listener; +extern const struct zwp_primary_selection_device_v1_listener primary_selection_device_listener; + +void selection_start( + struct terminal *term, int col, int row, + enum selection_kind new_kind, bool spaces_only); +void selection_update(struct terminal *term, int col, int row); +void selection_finalize( + struct seat *seat, struct terminal *term, uint32_t serial); +void selection_dirty_cells(struct terminal *term); +void selection_cancel(struct terminal *term); +void selection_extend( + struct seat *seat, struct terminal *term, + int col, int row, enum selection_kind kind); + +bool selection_on_rows(const struct terminal *term, int start, int end); + +void selection_scroll_up(struct terminal *term, int rows); +void selection_scroll_down(struct terminal *term, int rows); +void selection_view_up(struct terminal *term, int new_view); +void selection_view_down(struct terminal *term, int new_view); + +void selection_clipboard_unset(struct seat *seat); +void selection_primary_unset(struct seat *seat); + +bool selection_clipboard_has_data(const struct seat *seat); +bool selection_primary_has_data(const struct seat *seat); + +char *selection_to_text(const struct terminal *term); +void selection_to_clipboard( + struct seat *seat, struct terminal *term, uint32_t serial); +void selection_from_clipboard( + struct seat *seat, struct terminal *term, uint32_t serial); +void selection_to_primary( + struct seat *seat, struct terminal *term, uint32_t serial); +void selection_from_primary(struct seat *seat, struct terminal *term); + +/* Copy text *to* primary/clipboard */ +bool text_to_clipboard( + struct seat *seat, struct terminal *term, char *text, uint32_t serial); +bool text_to_primary( + struct seat *seat, struct terminal *term, char *text, uint32_t serial); + +/* + * Copy text *from* primary/clipboard + * + * Note that these are asynchronous; they *will* return + * immediately. The 'cb' callback will be called 0..n times with + * clipboard data. When done (or on error), the 'done' callback is + * called. + * + * As such, keep this in mind: + * - The 'user' context must not be stack allocated + * - Don't expect clipboard data to have been received when these + * functions return (it will *never* have been received at this + * point). + */ +void text_from_clipboard( + struct seat *seat, struct terminal *term, bool no_strip, + void (*cb)(char *data, size_t size, void *user), + void (*done)(void *user), void *user); + +void text_from_primary( + struct seat *seat, struct terminal *term, bool no_strip, + void (*cb)(char *data, size_t size, void *user), + void (*dont)(void *user), void *user); + +void selection_start_scroll_timer( + struct terminal *term, int interval_ns, + enum selection_scroll_direction direction, int col); +void selection_stop_scroll_timer(struct terminal *term); + +void selection_find_word_boundary_left( + const struct terminal *term, struct coord *pos, bool spaces_only); +void selection_find_word_boundary_right( + const struct terminal *term, struct coord *pos, bool spaces_only, + bool stop_on_space_to_word_boundary); + +struct coord selection_get_start(const struct terminal *term); +struct coord selection_get_end(const struct terminal *term); diff --git a/server.c b/server.c new file mode 100644 index 0000000..2596332 --- /dev/null +++ b/server.c @@ -0,0 +1,690 @@ +#include "server.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +#define LOG_MODULE "server" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "client-protocol.h" +#include "terminal.h" +#include "util.h" +#include "wayland.h" +#include "xmalloc.h" + +#define NON_ZERO_OPT (INT_MIN / 7) + +struct client; +struct terminal_instance; + +struct server { + struct config *conf; + struct fdm *fdm; + struct reaper *reaper; + struct wayland *wayl; + + int fd; + const char *sock_path; + + tll(struct client *) clients; + tll(struct terminal_instance *) terminals; +}; + +struct client { + struct server *server; + int fd; + + struct { + uint8_t *data; + size_t left; + size_t idx; + } buffer; + + struct terminal_instance *instance; +}; +static void client_destroy(struct client *client); + +struct terminal_instance { + struct terminal *terminal; + struct server *server; + struct client *client; + struct config *conf; +}; +static void instance_destroy(struct terminal_instance *instance, int exit_code); + +static void +client_destroy(struct client *client) +{ + if (client == NULL) + return; + + if (client->instance != NULL) { + LOG_WARN("client FD=%d: terminal still alive", client->fd); + client->instance->client = NULL; + instance_destroy(client->instance, 1); + } + + if (client->fd != -1) { + LOG_DBG("client FD=%d: disconnected", client->fd); + fdm_del(client->server->fdm, client->fd); + } + + tll_foreach(client->server->clients, it) { + if (it->item == client) { + tll_remove(client->server->clients, it); + break; + } + } + + free(client->buffer.data); + free(client); +} + +static void +client_send_exit_code(struct client *client, int exit_code) +{ + if (client->fd == -1) + return; + + if (write(client->fd, &exit_code, sizeof(exit_code)) != sizeof(exit_code)) + LOG_ERRNO("failed to write slave exit code to client"); +} + +static void +instance_destroy(struct terminal_instance *instance, int exit_code) +{ + if (instance->terminal != NULL) + term_destroy(instance->terminal); + + tll_foreach(instance->server->terminals, it) { + if (it->item == instance) { + tll_remove(instance->server->terminals, it); + break; + } + } + + if (instance->client != NULL) { + instance->client->instance = NULL; + client_send_exit_code(instance->client, exit_code); + client_destroy(instance->client); + } + + /* TODO: clone server conf completely, so that we can just call + * conf_destroy() here */ + if (instance->conf != NULL) { + config_free(instance->conf); + free(instance->conf); + } + free(instance); + +} + +static void +term_shutdown_handler(void *data, int exit_code) +{ + struct terminal_instance *instance = data; + + instance->terminal = NULL; + instance_destroy(instance, exit_code); +} + +static bool +fdm_client(struct fdm *fdm, int fd, int events, void *data) +{ + struct client *client = data; + struct server *server = client->server; + + char **argv = NULL; + config_override_t overrides = tll_init(); + char **envp = NULL; + + if (events & EPOLLHUP) + goto shutdown; + + xassert(events & EPOLLIN); + + if (client->instance != NULL) { + struct client_ipc_hdr ipc_hdr; + ssize_t count = read(fd, &ipc_hdr, sizeof(ipc_hdr)); + + if (count != sizeof(ipc_hdr)) { + LOG_WARN("client unexpectedly sent %zd bytes", count); + return true; /* TODO: shutdown instead? */ + } + + switch (ipc_hdr.ipc_code) { + case FOOT_IPC_SIGUSR: { + xassert(ipc_hdr.size == sizeof(struct client_ipc_sigusr)); + + struct client_ipc_sigusr sigusr; + count = read(fd, &sigusr, sizeof(sigusr)); + if (count < 0) { + LOG_ERRNO("failed to read SIGUSR IPC data from client"); + return true; /* TODO: shutdown instead? */ + } + + if ((size_t)count != sizeof(sigusr)) { + LOG_ERR("failed to read SIGUSR IPC data from client"); + return true; /* TODO: shutdown instead? */ + } + + switch (sigusr.signo) { + case SIGUSR1: + term_theme_switch_to_dark(client->instance->terminal); + break; + + case SIGUSR2: + term_theme_switch_to_light(client->instance->terminal); + break; + + default: + LOG_ERR( + "client sent bad SIGUSR number: %d " + "(expected SIGUSR1=%d or SIGUSR2=%d)", + sigusr.signo, SIGUSR1, SIGUSR2); + break; + } + + return true; + } + + default: + LOG_WARN( + "client sent unrecognized IPC (0x%04x), ignoring %hhu bytes", + ipc_hdr.ipc_code, ipc_hdr.size); + + /* TODO: slightly broken, since not all data is guaranteed + to be readable yet */ + uint8_t dummy[ipc_hdr.size]; + (void)!!read(fd, dummy, ipc_hdr.size); + return true; + } + } + + if (client->buffer.data == NULL) { + /* + * We haven't received any data yet - the first thing the + * client sends is the total size of the initialization + * data. + */ + uint32_t total_len; + ssize_t count = recv(fd, &total_len, sizeof(total_len), 0); + if (count < 0) { + LOG_ERRNO("failed to read total length"); + goto shutdown; + } + + if (count != sizeof(total_len)) { + LOG_ERR("client did not send setup packet size"); + goto shutdown; + } + + const uint32_t max_size = 128 * 1024; + if (total_len > max_size) { + LOG_ERR("client wants to send too large setup packet (%u > %u)", + total_len, max_size); + goto shutdown; + } + + LOG_DBG("total len: %u", total_len); + client->buffer.data = xmalloc(total_len + 1); + client->buffer.left = total_len; + client->buffer.idx = 0; + + /* Prevent our strlen() calls to run outside */ + client->buffer.data[total_len] = '\0'; + return true; /* Let FDM trigger again when we have more data */ + } + + /* Keep filling our buffer of initialization data */ + ssize_t count = recv( + fd, &client->buffer.data[client->buffer.idx], client->buffer.left, 0); + + if (count < 0) { + LOG_ERRNO("failed to read"); + goto shutdown; + } + + client->buffer.idx += count; + client->buffer.left -= count; + + if (client->buffer.left > 0) { + /* Not done yet */ + return true; + } + + if (tll_length(server->wayl->monitors) == 0) { + LOG_ERR("no monitors available for new terminal"); + client_send_exit_code(client, -26); + goto shutdown; + } + + /* All initialization data received - time to instantiate a terminal! */ + + xassert(client->instance == NULL); + xassert(client->buffer.data != NULL); + xassert(client->buffer.left == 0); + + /* + * Parse the received buffer, verifying lengths etc + */ + +#define CHECK_BUF(sz) do { \ + if (p + (sz) > end) \ + goto shutdown; \ + } while (0) + +#define CHECK_BUF_AND_NULL(sz) do { \ + CHECK_BUF(sz); \ + if (sz == 0) \ + goto shutdown; \ + if (p[sz - 1] != '\0') \ + goto shutdown; \ + } while (0) + + uint8_t *p = client->buffer.data; + const uint8_t *end = &client->buffer.data[client->buffer.idx]; + + struct client_data cdata; + CHECK_BUF(sizeof(cdata)); + memcpy(&cdata, p, sizeof(cdata)); + p += sizeof(cdata); + + CHECK_BUF_AND_NULL(cdata.cwd_len); + const char *cwd = (const char *)p; p += cdata.cwd_len; + LOG_DBG("CWD = %.*s", cdata.cwd_len, cwd); + + /* XDGA token */ + const char *token = NULL; + if (cdata.xdga_token) { + + CHECK_BUF_AND_NULL(cdata.token_len); + token = (const char *)p; p += cdata.token_len; + LOG_DBG("XDGA = %.*s", cdata.token_len, token); + } else { + LOG_DBG("No XDGA token"); + } + + /* Overrides */ + for (uint16_t i = 0; i < cdata.override_count; i++) { + struct client_string arg; + CHECK_BUF(sizeof(arg)); + memcpy(&arg, p, sizeof(arg)); p += sizeof(arg); + + CHECK_BUF_AND_NULL(arg.len); + const char *str = (const char *)p; + p += arg.len; + + tll_push_back(overrides, xstrdup(str)); + } + + /* argv */ + argv = xcalloc(cdata.argc + 1, sizeof(argv[0])); + for (uint16_t i = 0; i < cdata.argc; i++) { + struct client_string arg; + CHECK_BUF(sizeof(arg)); + memcpy(&arg, p, sizeof(arg)); p += sizeof(arg); + + CHECK_BUF_AND_NULL(arg.len); + argv[i] = (char *)p; p += arg.len; + LOG_DBG("argv[%hu] = %.*s", i, arg.len, argv[i]); + } + + /* envp */ + envp = cdata.env_count != 0 + ? xcalloc(cdata.env_count + 1, sizeof(envp[0])) + : NULL; + + for (uint16_t i = 0; i < cdata.env_count; i++) { + struct client_string e; + CHECK_BUF(sizeof(e)); + memcpy(&e, p, sizeof(e)); p += sizeof(e); + + CHECK_BUF_AND_NULL(e.len); + envp[i] = (char *)p; p += e.len; + LOG_DBG("env[%hu] = %.*s", i, e.len, envp[i]); + } + +#undef CHECK_BUF_AND_NULL +#undef CHECK_BUF + + struct terminal_instance *instance = xmalloc(sizeof(struct terminal_instance)); + + const bool need_to_clone_conf = + tll_length(overrides)> 0 || + cdata.hold != server->conf->hold_at_exit; + + struct config *conf = NULL; + if (need_to_clone_conf) { + conf = config_clone(server->conf); + + if (cdata.hold != server->conf->hold_at_exit) + conf->hold_at_exit = cdata.hold; + + config_override_apply(conf, &overrides, false); + + if (conf->tweak.font_monospace_warn && conf->fonts[0].count > 0) { + check_if_font_is_monospaced( + conf->fonts[0].arr[0].pattern, + &conf->notifications); + } + } + + *instance = (struct terminal_instance) { + .client = NULL, + .server = server, + .conf = conf, + }; + + instance->terminal = term_init( + conf != NULL ? conf : server->conf, + server->fdm, server->reaper, server->wayl, "footclient", cwd, token, + NULL, cdata.argc, argv, (const char *const *)envp, + &term_shutdown_handler, instance); + + if (instance->terminal == NULL) { + LOG_ERR("failed to instantiate new terminal"); + client_send_exit_code(client, -26); + instance_destroy(instance, -1); + goto shutdown; + } + + if (cdata.no_wait) { + // the server owns the instance + tll_push_back(server->terminals, instance); + client_send_exit_code(client, 0); + goto shutdown; + } else { + // the instance is attached to the client + instance->client = client; + client->instance = instance; + free(argv); + free(envp); + tll_free_and_free(overrides, free); + } + + return true; + +shutdown: + LOG_DBG("client FD=%d: disconnected", client->fd); + + free(argv); + free(envp); + tll_free_and_free(overrides, free); + fdm_del(fdm, fd); + client->fd = -1; + + if (client->instance != NULL && + !client->instance->terminal->shutdown.in_progress) + { + term_shutdown(client->instance->terminal); + } else + client_destroy(client); + + return true; +} + +static bool +fdm_server(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct server *server = data; + + struct sockaddr_un addr; + socklen_t addr_size = sizeof(addr); + int client_fd = accept4( + server->fd, (struct sockaddr *)&addr, &addr_size, SOCK_CLOEXEC | SOCK_NONBLOCK); + + if (client_fd == -1) { + LOG_ERRNO("failed to accept client connection"); + return false; + } + + struct client *client = xmalloc(sizeof(*client)); + *client = (struct client) { + .server = server, + .fd = client_fd, + }; + + if (!fdm_add(server->fdm, client_fd, EPOLLIN, &fdm_client, client)) { + close(client_fd); + free(client); + return false; + } + + LOG_DBG("client FD=%d: connected", client_fd); + tll_push_back(server->clients, client); + return true; +} + +enum connect_status {CONNECT_ERR, CONNECT_FAIL, CONNECT_SUCCESS}; + +static enum connect_status +try_connect(const char *sock_path) +{ + enum connect_status ret = CONNECT_ERR; + + int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (fd == -1) { + LOG_ERRNO("failed to create UNIX socket"); + goto err; + } + + struct sockaddr_un addr = {.sun_family = AF_UNIX}; + strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1); + + switch (connect(fd, (struct sockaddr *)&addr, sizeof(addr))) { + case 0: + ret = CONNECT_SUCCESS; + break; + + case -1: + LOG_DBG("connect() failed: %s", strerror(errno)); + ret = CONNECT_FAIL; + break; + } + +err: + if (fd != -1) + close(fd); + return ret; +} + +static bool +prepare_socket(int fd) +{ + int flags = fcntl(fd, F_GETFD); + if (flags < 0) { + LOG_ERRNO("failed to get file descriptors flag for passed socket"); + return false; + } + if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) == -1) { + LOG_ERRNO("failed to set FD_CLOEXEC for passed socket"); + return false; + } + + flags = fcntl(fd, F_GETFL); + if (flags < 0) { + LOG_ERRNO("failed to get file status flags for passed socket"); + return false; + } + if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1) { + LOG_ERRNO("failed to set non-blocking mode on passed socket"); + return false; + } + + int const socket_options[] = { SO_DOMAIN, SO_ACCEPTCONN, SO_TYPE }; + int const socket_options_values[] = { AF_UNIX, NON_ZERO_OPT, SOCK_STREAM}; + char const * const socket_options_names[] = { "SO_DOMAIN", "SO_ACCEPTCONN", "SO_TYPE" }; + + xassert(ALEN(socket_options) == ALEN(socket_options_values)); + xassert(ALEN(socket_options) == ALEN(socket_options_names)); + + int socket_option = 0; + socklen_t len; + for (size_t i = 0; i < ALEN(socket_options) ; i++) { + len = sizeof(socket_option); + if (getsockopt(fd, SOL_SOCKET, socket_options[i], &socket_option, &len) == -1 || + len != sizeof(socket_option)) { + LOG_ERRNO("failed to read socket option from passed file descriptor"); + return false; + } + if (socket_options_values[i] == NON_ZERO_OPT && socket_option) + socket_option = NON_ZERO_OPT; + if (socket_option != socket_options_values[i]) { + LOG_ERR("wrong socket value for socket option '%s' on passed file descriptor", + socket_options_names[i]); + return false; + } + } + + return true; +} + +struct server * +server_init(struct config *conf, struct fdm *fdm, struct reaper *reaper, + struct wayland *wayl) +{ + int fd; + struct server *server = NULL; + const char *sock_path = conf->server_socket_path; + char *end; + + errno = 0; + fd = strtol(sock_path, &end, 10); + if (*end == '\0' && *sock_path != '\0') + { + if (!prepare_socket(fd)) + goto err; + LOG_DBG("we've been started by socket activation, using passed socket"); + sock_path = NULL; + } + else { + LOG_DBG("no suitable pre-existing socket found, creating our own"); + fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (fd == -1) { + LOG_ERRNO("failed to create UNIX socket"); + return NULL; + } + + switch (try_connect(sock_path)) { + case CONNECT_FAIL: + break; + + case CONNECT_SUCCESS: + LOG_ERR("%s is already accepting connections; is 'foot --server' already running", sock_path); + /* FALLTHROUGH */ + + case CONNECT_ERR: + goto err; + } + + unlink(sock_path); + + struct sockaddr_un addr = {.sun_family = AF_UNIX}; + strncpy(addr.sun_path, sock_path, sizeof(addr.sun_path) - 1); + + if (bind(fd, (const struct sockaddr *)&addr, sizeof(addr)) < 0) { + LOG_ERRNO("%s: failed to bind", addr.sun_path); + goto err; + } + + if (listen(fd, 0) < 0) { + LOG_ERRNO("%s: failed to listen", addr.sun_path); + goto err; + } + } + + server = malloc(sizeof(*server)); + if (unlikely(server == NULL)) { + LOG_ERRNO("malloc() failed"); + goto err; + } + + *server = (struct server) { + .conf = conf, + .fdm = fdm, + .reaper = reaper, + .wayl = wayl, + + .fd = fd, + .sock_path = sock_path, + + .clients = tll_init(), + .terminals = tll_init(), + }; + + if (!fdm_add(fdm, fd, EPOLLIN, &fdm_server, server)) + goto err; + + LOG_INFO("accepting connections on %s", sock_path != NULL ? sock_path : "socket provided through socket activation"); + + return server; + +err: + free(server); + if (fd != -1) + close(fd); + return NULL; +} + +void +server_destroy(struct server *server) +{ + if (server == NULL) + return; + + LOG_DBG("server destroy, %zu clients still alive", + tll_length(server->clients)); + + tll_foreach(server->clients, it) { + client_send_exit_code(it->item, -26); + client_destroy(it->item); + } + + tll_free(server->clients); + + tll_foreach(server->terminals, it) + instance_destroy(it->item, 1); + + tll_free(server->terminals); + + fdm_del(server->fdm, server->fd); + if (server->sock_path != NULL) + unlink(server->sock_path); + free(server); +} + +void +server_global_theme_switch_to_dark(struct server *server) +{ + server->conf->initial_color_theme = COLOR_THEME_DARK; + tll_foreach(server->clients, it) + term_theme_switch_to_dark(it->item->instance->terminal); + tll_foreach(server->terminals, it) + term_theme_switch_to_dark(it->item->terminal); +} + +void +server_global_theme_switch_to_light(struct server *server) +{ + server->conf->initial_color_theme = COLOR_THEME_LIGHT; + tll_foreach(server->clients, it) + term_theme_switch_to_light(it->item->instance->terminal); + tll_foreach(server->terminals, it) + term_theme_switch_to_light(it->item->terminal); +} diff --git a/server.h b/server.h new file mode 100644 index 0000000..683ad74 --- /dev/null +++ b/server.h @@ -0,0 +1,14 @@ +#pragma once + +#include "fdm.h" +#include "config.h" +#include "reaper.h" +#include "wayland.h" + +struct server; +struct server *server_init(struct config *conf, struct fdm *fdm, + struct reaper *reaper, struct wayland *wayl); +void server_destroy(struct server *server); + +void server_global_theme_switch_to_dark(struct server *server); +void server_global_theme_switch_to_light(struct server *server); diff --git a/shm-formats.h b/shm-formats.h new file mode 100644 index 0000000..a73ba1f --- /dev/null +++ b/shm-formats.h @@ -0,0 +1,138 @@ +#pragma once + +#include + +#if defined(_DEBUG) +static const struct shm_formats { + uint32_t format; + const char *description; +} shm_formats[] = { + {WL_SHM_FORMAT_ARGB8888, "ARGB8888"}, + {WL_SHM_FORMAT_XRGB8888, "XRGB8888"}, + {WL_SHM_FORMAT_C8, "C8"}, + {WL_SHM_FORMAT_RGB332, "RGB332"}, + {WL_SHM_FORMAT_BGR233, "BGR233"}, + {WL_SHM_FORMAT_XRGB4444, "XRGB4444"}, + {WL_SHM_FORMAT_XBGR4444, "XBGR4444"}, + {WL_SHM_FORMAT_RGBX4444, "RGBX4444"}, + {WL_SHM_FORMAT_BGRX4444, "BGRX4444"}, + {WL_SHM_FORMAT_ARGB4444, "ARGB4444"}, + {WL_SHM_FORMAT_ABGR4444, "ABGR4444"}, + {WL_SHM_FORMAT_RGBA4444, "RGBA4444"}, + {WL_SHM_FORMAT_BGRA4444, "BGRA4444"}, + {WL_SHM_FORMAT_XRGB1555, "XRGB1555"}, + {WL_SHM_FORMAT_XBGR1555, "XBGR1555"}, + {WL_SHM_FORMAT_RGBX5551, "RGBX5551"}, + {WL_SHM_FORMAT_BGRX5551, "BGRX5551"}, + {WL_SHM_FORMAT_ARGB1555, "ARGB1555"}, + {WL_SHM_FORMAT_ABGR1555, "ABGR1555"}, + {WL_SHM_FORMAT_RGBA5551, "RGBA5551"}, + {WL_SHM_FORMAT_BGRA5551, "BGRA5551"}, + {WL_SHM_FORMAT_RGB565, "RGB565"}, + {WL_SHM_FORMAT_BGR565, "BGR565"}, + {WL_SHM_FORMAT_RGB888, "RGB888"}, + {WL_SHM_FORMAT_BGR888, "BGR888"}, + {WL_SHM_FORMAT_XBGR8888, "XBGR8888"}, + {WL_SHM_FORMAT_RGBX8888, "RGBX8888"}, + {WL_SHM_FORMAT_BGRX8888, "BGRX8888"}, + {WL_SHM_FORMAT_ABGR8888, "ABGR8888"}, + {WL_SHM_FORMAT_RGBA8888, "RGBA8888"}, + {WL_SHM_FORMAT_BGRA8888, "BGRA8888"}, + {WL_SHM_FORMAT_XRGB2101010, "XRGB2101010"}, + {WL_SHM_FORMAT_XBGR2101010, "XBGR2101010"}, + {WL_SHM_FORMAT_RGBX1010102, "RGBX1010102"}, + {WL_SHM_FORMAT_BGRX1010102, "BGRX1010102"}, + {WL_SHM_FORMAT_ARGB2101010, "ARGB2101010"}, + {WL_SHM_FORMAT_ABGR2101010, "ABGR2101010"}, + {WL_SHM_FORMAT_RGBA1010102, "RGBA1010102"}, + {WL_SHM_FORMAT_BGRA1010102, "BGRA1010102"}, + {WL_SHM_FORMAT_YUYV, "YUYV"}, + {WL_SHM_FORMAT_YVYU, "YVYU"}, + {WL_SHM_FORMAT_UYVY, "UYVY"}, + {WL_SHM_FORMAT_VYUY, "VYUY"}, + {WL_SHM_FORMAT_AYUV, "AYUV"}, + {WL_SHM_FORMAT_NV12, "NV12"}, + {WL_SHM_FORMAT_NV21, "NV21"}, + {WL_SHM_FORMAT_NV16, "NV16"}, + {WL_SHM_FORMAT_NV61, "NV61"}, + {WL_SHM_FORMAT_YUV410, "YUV410"}, + {WL_SHM_FORMAT_YVU410, "YVU410"}, + {WL_SHM_FORMAT_YUV411, "YUV411"}, + {WL_SHM_FORMAT_YVU411, "YVU411"}, + {WL_SHM_FORMAT_YUV420, "YUV420"}, + {WL_SHM_FORMAT_YVU420, "YVU420"}, + {WL_SHM_FORMAT_YUV422, "YUV422"}, + {WL_SHM_FORMAT_YVU422, "YVU422"}, + {WL_SHM_FORMAT_YUV444, "YUV444"}, + {WL_SHM_FORMAT_YVU444, "YVU444"}, + {WL_SHM_FORMAT_R8, "R8"}, + {WL_SHM_FORMAT_R16, "R16"}, + {WL_SHM_FORMAT_RG88, "RG88"}, + {WL_SHM_FORMAT_GR88, "GR88"}, + {WL_SHM_FORMAT_RG1616, "RG1616"}, + {WL_SHM_FORMAT_GR1616, "GR1616"}, + {WL_SHM_FORMAT_XRGB16161616F, "XRGB16161616F"}, + {WL_SHM_FORMAT_XBGR16161616F, "XBGR16161616F"}, + {WL_SHM_FORMAT_ARGB16161616F, "ARGB16161616F"}, + {WL_SHM_FORMAT_ABGR16161616F, "ABGR16161616F"}, + {WL_SHM_FORMAT_XYUV8888, "XYUV8888"}, + {WL_SHM_FORMAT_VUY888, "VUY888"}, + {WL_SHM_FORMAT_VUY101010, "VUY101010"}, + {WL_SHM_FORMAT_Y210, "Y210"}, + {WL_SHM_FORMAT_Y212, "Y212"}, + {WL_SHM_FORMAT_Y216, "Y216"}, + {WL_SHM_FORMAT_Y410, "Y410"}, + {WL_SHM_FORMAT_Y412, "Y412"}, + {WL_SHM_FORMAT_Y416, "Y416"}, + {WL_SHM_FORMAT_XVYU2101010, "XVYU2101010"}, + {WL_SHM_FORMAT_XVYU12_16161616, "XVYU12_16161616"}, + {WL_SHM_FORMAT_XVYU16161616, "XVYU16161616"}, + {WL_SHM_FORMAT_Y0L0, "Y0L0"}, + {WL_SHM_FORMAT_X0L0, "X0L0"}, + {WL_SHM_FORMAT_Y0L2, "Y0L2"}, + {WL_SHM_FORMAT_X0L2, "X0L2"}, + {WL_SHM_FORMAT_YUV420_8BIT, "YUV420_8BIT"}, + {WL_SHM_FORMAT_YUV420_10BIT, "YUV420_10BIT"}, + {WL_SHM_FORMAT_XRGB8888_A8, "XRGB8888_A8"}, + {WL_SHM_FORMAT_XBGR8888_A8, "XBGR8888_A8"}, + {WL_SHM_FORMAT_RGBX8888_A8, "RGBX8888_A8"}, + {WL_SHM_FORMAT_BGRX8888_A8, "BGRX8888_A8"}, + {WL_SHM_FORMAT_RGB888_A8, "RGB888_A8"}, + {WL_SHM_FORMAT_BGR888_A8, "BGR888_A8"}, + {WL_SHM_FORMAT_RGB565_A8, "RGB565_A8"}, + {WL_SHM_FORMAT_BGR565_A8, "BGR565_A8"}, + {WL_SHM_FORMAT_NV24, "NV24"}, + {WL_SHM_FORMAT_NV42, "NV42"}, + {WL_SHM_FORMAT_P210, "P210"}, + {WL_SHM_FORMAT_P010, "P010"}, + {WL_SHM_FORMAT_P012, "P012"}, + {WL_SHM_FORMAT_P016, "P016"}, + {WL_SHM_FORMAT_AXBXGXRX106106106106, "AXBXGXRX106106106106"}, + {WL_SHM_FORMAT_NV15, "NV15"}, + {WL_SHM_FORMAT_Q410, "Q410"}, + {WL_SHM_FORMAT_Q401, "Q401"}, +#if WAYLAND_VERSION_MAJOR > 1 || WAYLAND_VERSION_MINOR >= 20 + {WL_SHM_FORMAT_XRGB16161616, "XRGB16161616"}, + {WL_SHM_FORMAT_XBGR16161616, "XBGR16161616"}, + {WL_SHM_FORMAT_ARGB16161616, "ARGB16161616"}, + {WL_SHM_FORMAT_ABGR16161616, "ABGR16161616"}, +#endif +#if WAYLAND_VERSION_MAJOR > 1 || WAYLAND_VERSION_MINOR >= 23 + {WL_SHM_FORMAT_C1, "C1"}, + {WL_SHM_FORMAT_C2, "C2"}, + {WL_SHM_FORMAT_C4, "C4"}, + {WL_SHM_FORMAT_D1, "D1"}, + {WL_SHM_FORMAT_D2, "D2"}, + {WL_SHM_FORMAT_D4, "D4"}, + {WL_SHM_FORMAT_D8, "D8"}, + {WL_SHM_FORMAT_R1, "R1"}, + {WL_SHM_FORMAT_R2, "R2"}, + {WL_SHM_FORMAT_R4, "R4"}, + {WL_SHM_FORMAT_R10, "R10"}, + {WL_SHM_FORMAT_R12, "R12"}, + {WL_SHM_FORMAT_AVUY8888, "AVUY8888"}, + {WL_SHM_FORMAT_XVUY8888, "XVUY8888"}, + {WL_SHM_FORMAT_P030, "P030"}, +#endif +}; +#endif diff --git a/shm.c b/shm.c new file mode 100644 index 0000000..5c1573a --- /dev/null +++ b/shm.c @@ -0,0 +1,1109 @@ +#include "shm.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +#define LOG_MODULE "shm" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" +#include "macros.h" +#include "stride.h" +#include "xmalloc.h" + +#if !defined(MAP_UNINITIALIZED) + #define MAP_UNINITIALIZED 0 +#endif + +#if !defined(MFD_NOEXEC_SEAL) + #define MFD_NOEXEC_SEAL 0 +#endif + +#define TIME_SCROLL 0 + +#define FORCED_DOUBLE_BUFFERING 0 + +/* + * Maximum memfd size allowed. + * + * On 64-bit, we could in theory use up to 2GB (wk_shm_create_pool() + * is limited to int32_t), since we never mmap() the entire region. + * + * The compositor is different matter - it needs to mmap() the entire + * range, and *keep* the mapping for as long as is has buffers + * referencing it (thus - always). And if we open multiple terminals, + * then the required address space multiples... + * + * That said, 128TB (the total amount of available user address space + * on 64-bit) is *a lot*; we can fit 67108864 2GB memfds into + * that. But, let's be conservative for now. + * + * On 32-bit the available address space is too small and SHM + * scrolling is disabled. + * + * Note: this is the _default_ size. It can be overridden by calling + * shm_set_max_pool_size(); + */ +static off_t max_pool_size = 512 * 1024 * 1024; + +static bool can_punch_hole = false; +static bool can_punch_hole_initialized = false; + +static size_t min_stride_alignment = 0; + +struct buffer_pool { + int fd; /* memfd */ + struct wl_shm_pool *wl_pool; + + void *real_mmapped; /* Address returned from mmap */ + size_t mmap_size; /* Size of mmap (>= size) */ + + size_t ref_count; +}; + +struct buffer_chain; +struct buffer_private { + struct buffer public; + struct buffer_chain *chain; + + size_t ref_count; + bool busy; /* Owned by compositor */ + + struct buffer_pool *pool; + off_t offset; /* Offset into memfd where data begins */ + size_t size; + + bool scrollable; + + void (*release_cb)(struct buffer *buf, void *data); + void *cb_data; +}; + +struct buffer_chain { + tll(struct buffer_private *) bufs; + struct wl_shm *shm; + size_t pix_instances; + bool scrollable; + + pixman_format_code_t pixman_fmt; + enum wl_shm_format shm_format; + + void (*release_cb)(struct buffer *buf, void *data); + void *cb_data; +}; + +static tll(struct buffer_private *) deferred; + +#undef MEASURE_SHM_ALLOCS +#if defined(MEASURE_SHM_ALLOCS) +static size_t max_alloced = 0; +#endif + +void +shm_set_max_pool_size(off_t _max_pool_size) +{ + max_pool_size = _max_pool_size; +} + +void +shm_set_min_stride_alignment(size_t _min_stride_alignment) +{ + min_stride_alignment = _min_stride_alignment; +} + +static void +buffer_destroy_dont_close(struct buffer *buf) +{ + if (buf->pix != NULL) { + for (size_t i = 0; i < buf->pix_instances; i++) + if (buf->pix[i] != NULL) + pixman_image_unref(buf->pix[i]); + } + + if (buf->wl_buf != NULL) + wl_buffer_destroy(buf->wl_buf); + + free(buf->pix); + buf->pix = NULL; + buf->wl_buf = NULL; + buf->data = NULL; +} + +static void +pool_unref(struct buffer_pool *pool) +{ + if (pool == NULL) + return; + + xassert(pool->ref_count > 0); + pool->ref_count--; + + if (pool->ref_count > 0) + return; + + if (pool->real_mmapped != MAP_FAILED) + munmap(pool->real_mmapped, pool->mmap_size); + if (pool->wl_pool != NULL) + wl_shm_pool_destroy(pool->wl_pool); + if (pool->fd >= 0) + close(pool->fd); + + pool->real_mmapped = MAP_FAILED; + pool->wl_pool = NULL; + pool->fd = -1; + free(pool); +} + +static void +buffer_destroy(struct buffer_private *buf) +{ + buffer_destroy_dont_close(&buf->public); + pool_unref(buf->pool); + buf->pool = NULL; + + for (size_t i = 0; i < buf->public.pix_instances; i++) + pixman_region32_fini(&buf->public.dirty[i]); + free(buf->public.dirty); + free(buf); +} + +static bool +buffer_unref_no_remove_from_chain(struct buffer_private *buf) +{ + xassert(buf->ref_count > 0); + buf->ref_count--; + + if (buf->ref_count > 0) + return false; + + if (buf->busy) + tll_push_back(deferred, buf); + else + buffer_destroy(buf); + return true; +} + +void +shm_fini(void) +{ + LOG_DBG("deferred buffers: %zu", tll_length(deferred)); + + tll_foreach(deferred, it) { + buffer_destroy(it->item); + tll_remove(deferred, it); + } + +#if defined(MEASURE_SHM_ALLOCS) && MEASURE_SHM_ALLOCS + LOG_INFO("max total allocations was: %zu MB", max_alloced / 1024 / 1024); +#endif +} + +static void +buffer_release(void *data, struct wl_buffer *wl_buffer) +{ + struct buffer_private *buffer = data; + + xassert(buffer->public.wl_buf == wl_buffer); + xassert(buffer->busy); + buffer->busy = false; + + if (buffer->ref_count == 0) { + bool found = false; + tll_foreach(deferred, it) { + if (it->item == buffer) { + found = true; + tll_remove(deferred, it); + break; + } + } + + buffer_destroy(buffer); + + xassert(found); + if (!found) + LOG_WARN("deferred delete: buffer not on the 'deferred' list"); + } else { + if (buffer->release_cb != NULL) { + buffer->release_cb(&buffer->public, buffer->cb_data); + } + } +} + +static const struct wl_buffer_listener buffer_listener = { + .release = &buffer_release, +}; + +static size_t +page_size(void) +{ + static size_t size = 0; + if (size == 0) { + long n = sysconf(_SC_PAGE_SIZE); + if (n <= 0) { + LOG_ERRNO("failed to get page size"); + size = 4096; + } else { + size = (size_t)n; + } + } + xassert(size > 0); + return size; +} + +static bool +instantiate_offset(struct buffer_private *buf, off_t new_offset) +{ + xassert(buf->public.data == NULL); + xassert(buf->public.pix == NULL); + xassert(buf->public.wl_buf == NULL); + xassert(buf->pool != NULL); + + const struct buffer_pool *pool = buf->pool; + + void *mmapped = MAP_FAILED; + struct wl_buffer *wl_buf = NULL; + pixman_image_t **pix = xcalloc(buf->public.pix_instances, sizeof(pix[0])); + + mmapped = (uint8_t *)pool->real_mmapped + new_offset; + + wl_buf = wl_shm_pool_create_buffer( + pool->wl_pool, new_offset, + buf->public.width, buf->public.height, buf->public.stride, + buf->chain->shm_format); + + if (wl_buf == NULL) { + LOG_ERR("failed to create SHM buffer"); + goto err; + } + + /* One pixman image for each worker thread (do we really need multiple?) */ + for (size_t i = 0; i < buf->public.pix_instances; i++) { + pix[i] = pixman_image_create_bits_no_clear( + buf->chain->pixman_fmt, + buf->public.width, buf->public.height, + (uint32_t *)mmapped, buf->public.stride); + + if (pix[i] == NULL) { + LOG_ERR("failed to create pixman image"); + goto err; + } + } + + buf->public.data = mmapped; + buf->public.wl_buf = wl_buf; + buf->public.pix = pix; + buf->offset = new_offset; + + wl_buffer_add_listener(wl_buf, &buffer_listener, buf); + return true; + +err: + if (pix != NULL) { + for (size_t i = 0; i < buf->public.pix_instances; i++) + if (pix[i] != NULL) + pixman_image_unref(pix[i]); + } + free(pix); + if (wl_buf != NULL) + wl_buffer_destroy(wl_buf); + + abort(); + return false; +} + +static void NOINLINE +get_new_buffers(struct buffer_chain *chain, size_t count, + int widths[static count], int heights[static count], + struct buffer *bufs[static count], bool immediate_purge) +{ + xassert(count == 1 || !chain->scrollable); + /* + * No existing buffer available. Create a new one by: + * + * 1. open a memory backed "file" with memfd_create() + * 2. mmap() the memory file, to be used by the pixman image + * 3. create a wayland shm buffer for the same memory file + * + * The pixman image and the wayland buffer are now sharing memory. + */ + + int stride[count]; + int sizes[count]; + + size_t total_size = 0; + for (size_t i = 0; i < count; i++) { + stride[i] = stride_for_format_and_width( + chain->pixman_fmt, widths[i]); + + if (min_stride_alignment > 0) { + const size_t m = min_stride_alignment; + stride[i] = (stride[i] + m - 1) / m * m; + } + + xassert(min_stride_alignment == 0 || stride[i] % min_stride_alignment == 0); + sizes[i] = stride[i] * heights[i]; + total_size += sizes[i]; + } + if (total_size == 0) + return; + + int pool_fd = -1; + + void *real_mmapped = MAP_FAILED; + struct wl_shm_pool *wl_pool = NULL; + struct buffer_pool *pool = NULL; + + /* Backing memory for SHM */ +#if defined(MEMFD_CREATE) + /* + * Older kernels reject MFD_NOEXEC_SEAL with EINVAL. Try first + * *with* it, and if that fails, try again *without* it. + */ + errno = 0; + pool_fd = memfd_create( + "foot-wayland-shm-buffer-pool", + MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL); + + if (pool_fd < 0 && errno == EINVAL && MFD_NOEXEC_SEAL != 0) { + pool_fd = memfd_create( + "foot-wayland-shm-buffer-pool", MFD_CLOEXEC | MFD_ALLOW_SEALING); + } + +#elif defined(__FreeBSD__) + // memfd_create on FreeBSD 13 is SHM_ANON without sealing support + pool_fd = shm_open(SHM_ANON, O_RDWR | O_CLOEXEC, 0600); +#else + char name[] = "/tmp/foot-wayland-shm-buffer-pool-XXXXXX"; + pool_fd = mkostemp(name, O_CLOEXEC); + unlink(name); +#endif + if (pool_fd == -1) { + LOG_ERRNO("failed to create SHM backing memory file"); + goto err; + } + + const size_t page_sz = page_size(); + +#if __SIZEOF_POINTER__ == 8 + off_t offset = chain->scrollable && max_pool_size > 0 + ? (max_pool_size / 4) & ~(page_sz - 1) + : 0; + off_t memfd_size = chain->scrollable && max_pool_size > 0 + ? max_pool_size + : total_size; +#else + off_t offset = 0; + off_t memfd_size = total_size; +#endif + + /* Page align */ + memfd_size = (memfd_size + page_sz - 1) & ~(page_sz - 1); + + LOG_DBG("memfd-size: %lu, initial offset: %lu", memfd_size, offset); + + if (ftruncate(pool_fd, memfd_size) == -1) { + LOG_ERRNO("failed to set size of SHM backing memory file"); + goto err; + } + + if (!can_punch_hole_initialized) { + can_punch_hole_initialized = true; +#if __SIZEOF_POINTER__ == 8 && defined(FALLOC_FL_PUNCH_HOLE) + can_punch_hole = fallocate( + pool_fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0, 1) == 0; + + if (!can_punch_hole) { + LOG_WARN( + "fallocate(FALLOC_FL_PUNCH_HOLE) not " + "supported (%s): expect lower performance", strerror(errno)); + } +#else + /* This is mostly to make sure we skip the warning issued + * above */ + can_punch_hole = false; +#endif + } + + if (chain->scrollable && !can_punch_hole) { + offset = 0; + memfd_size = total_size; + chain->scrollable = false; + + /* Page align */ + memfd_size = (memfd_size + page_sz - 1) & ~(page_sz - 1); + + if (ftruncate(pool_fd, memfd_size) < 0) { + LOG_ERRNO("failed to set size of SHM backing memory file"); + goto err; + } + } + + real_mmapped = mmap( + NULL, memfd_size, PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_UNINITIALIZED, pool_fd, 0); + + if (real_mmapped == MAP_FAILED) { + LOG_ERRNO("failed to mmap SHM backing memory file"); + goto err; + } + +#if defined(MEMFD_CREATE) + /* Seal file - we no longer allow any kind of resizing */ + /* TODO: wayland mmaps(PROT_WRITE), for some unknown reason, hence we cannot use F_SEAL_FUTURE_WRITE */ + if (fcntl(pool_fd, F_ADD_SEALS, + F_SEAL_GROW | F_SEAL_SHRINK | /*F_SEAL_FUTURE_WRITE |*/ F_SEAL_SEAL) < 0) + { + LOG_ERRNO("failed to seal SHM backing memory file"); + /* This is not a fatal error */ + } +#endif + + wl_pool = wl_shm_create_pool(chain->shm, pool_fd, memfd_size); + if (wl_pool == NULL) { + LOG_ERR("failed to create SHM pool"); + goto err; + } + + pool = xmalloc(sizeof(*pool)); + if (pool == NULL) { + LOG_ERRNO("failed to allocate buffer pool"); + goto err; + } + + *pool = (struct buffer_pool){ + .fd = pool_fd, + .wl_pool = wl_pool, + .real_mmapped = real_mmapped, + .mmap_size = memfd_size, + .ref_count = 0, + }; + + for (size_t i = 0; i < count; i++) { + if (sizes[i] == 0) { + bufs[i] = NULL; + continue; + } + + /* Push to list of available buffers, but marked as 'busy' */ + struct buffer_private *buf = xmalloc(sizeof(*buf)); + *buf = (struct buffer_private){ + .public = { + .width = widths[i], + .height = heights[i], + .stride = stride[i], + .pix_instances = chain->pix_instances, + .age = 1234, /* Force a full repaint */ + }, + .chain = chain, + .ref_count = immediate_purge ? 0 : 1, + .busy = true, + .pool = pool, + .offset = 0, + .size = sizes[i], + .scrollable = chain->scrollable, + .release_cb = chain->release_cb, + .cb_data = chain->cb_data, + }; + + if (!instantiate_offset(buf, offset)) { + free(buf); + goto err; + } + + if (immediate_purge) + tll_push_front(deferred, buf); + else + tll_push_front(chain->bufs, buf); + + buf->public.dirty = xmalloc( + chain->pix_instances * sizeof(buf->public.dirty[0])); + + for (size_t j = 0; j < chain->pix_instances; j++) + pixman_region32_init(&buf->public.dirty[j]); + + pool->ref_count++; + offset += buf->size; + bufs[i] = &buf->public; + } + +#if defined(MEASURE_SHM_ALLOCS) && MEASURE_SHM_ALLOCS + { + size_t currently_alloced = 0; + tll_foreach(buffers, it) + currently_alloced += it->item.size; + if (currently_alloced > max_alloced) + max_alloced = currently_alloced; + } +#endif + + if (!(bufs[0] && shm_can_scroll(bufs[0]))) { + /* We only need to keep the pool FD open if we're going to SHM + * scroll it */ + close(pool_fd); + pool->fd = -1; + } + + return; + +err: + pool_unref(pool); + if (wl_pool != NULL) + wl_shm_pool_destroy(wl_pool); + if (real_mmapped != MAP_FAILED) + munmap(real_mmapped, memfd_size); + if (pool_fd != -1) + close(pool_fd); + + /* We don't handle this */ + abort(); +} + +void +shm_did_not_use_buf(struct buffer *_buf) +{ + struct buffer_private *buf = (struct buffer_private *)_buf; + buf->busy = false; +} + +void +shm_get_many(struct buffer_chain *chain, size_t count, + int widths[static count], int heights[static count], + struct buffer *bufs[static count]) +{ + get_new_buffers(chain, count, widths, heights, bufs, true); +} + +struct buffer * +shm_get_buffer(struct buffer_chain *chain, int width, int height) +{ + LOG_DBG( + "chain=%p: looking for a reusable %dx%d buffer " + "among %zu potential buffers", + (void *)chain, width, height, tll_length(chain->bufs)); + + struct buffer_private *cached = NULL; + tll_foreach(chain->bufs, it) { + struct buffer_private *buf = it->item; + + if (buf->public.width != width || buf->public.height != height) { + LOG_DBG("purging mismatching buffer %p", (void *)buf); + if (buffer_unref_no_remove_from_chain(buf)) + tll_remove(chain->bufs, it); + continue; + } + + if (buf->busy) + buf->public.age++; + else +#if FORCED_DOUBLE_BUFFERING + if (buf->public.age == 0) + buf->public.age++; + else +#endif + { + if (cached == NULL) { + cached = buf; + } else { + /* We have multiple buffers eligible for + * reuse. Pick the "youngest" one, and mark the + * other one for purging */ + if (buf->public.age < cached->public.age) { + shm_unref(&cached->public); + cached = buf; + } else { + /* + * TODO: I think we _can_ use shm_unref() + * here... + * + * shm_unref() may remove 'it', but that + * should be safe; "our" tll_foreach() already + * holds the next pointer. + */ + if (buffer_unref_no_remove_from_chain(buf)) + tll_remove(chain->bufs, it); + } + } + } + } + + if (cached != NULL) { + LOG_DBG("reusing buffer %p from cache", (void *)cached); + cached->busy = true; + for (size_t i = 0; i < cached->public.pix_instances; i++) + pixman_region32_clear(&cached->public.dirty[i]); + xassert(cached->public.pix_instances == chain->pix_instances); + return &cached->public; + } + + struct buffer *ret; + get_new_buffers(chain, 1, &width, &height, &ret, false); + return ret; +} + +bool +shm_can_scroll(const struct buffer *_buf) +{ +#if __SIZEOF_POINTER__ == 8 + const struct buffer_private *buf = (const struct buffer_private *)_buf; + return can_punch_hole && max_pool_size > 0 && buf->scrollable; +#else + /* Not enough virtual address space in 32-bit */ + return false; +#endif +} + +#if __SIZEOF_POINTER__ == 8 && defined(FALLOC_FL_PUNCH_HOLE) +static bool +wrap_buffer(struct buffer_private *buf, off_t new_offset) +{ + struct buffer_pool *pool = buf->pool; + xassert(pool->ref_count == 1); + + /* We don't allow overlapping offsets */ + off_t UNUSED diff = new_offset < buf->offset + ? buf->offset - new_offset + : new_offset - buf->offset; + xassert(diff > buf->size); + + memcpy((uint8_t *)pool->real_mmapped + new_offset, + buf->public.data, + buf->size); + + off_t trim_ofs, trim_len; + if (new_offset > buf->offset) { + /* Trim everything *before* the new offset */ + trim_ofs = 0; + trim_len = new_offset; + } else { + /* Trim everything *after* the new buffer location */ + trim_ofs = new_offset + buf->size; + trim_len = pool->mmap_size - trim_ofs; + } + + if (fallocate( + pool->fd, + FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, + trim_ofs, trim_len) < 0) + { + LOG_ERRNO("failed to trim SHM backing memory file"); + return false; + } + + /* Re-instantiate pixman+wl_buffer+raw pointersw */ + buffer_destroy_dont_close(&buf->public); + return instantiate_offset(buf, new_offset); +} + +static bool +shm_scroll_forward(struct buffer_private *buf, int rows, + int top_margin, int top_keep_rows, + int bottom_margin, int bottom_keep_rows) +{ + struct buffer_pool *pool = buf->pool; + + xassert(can_punch_hole); + xassert(buf->busy); + xassert(buf->public.pix != NULL); + xassert(buf->public.wl_buf != NULL); + xassert(pool != NULL); + xassert(pool->ref_count == 1); + xassert(pool->fd >= 0); + + LOG_DBG("scrolling %d rows (%d bytes)", rows, rows * buf->public.stride); + + const off_t diff = rows * buf->public.stride; + xassert(rows > 0); + xassert(diff < buf->size); + + if (buf->offset + diff + buf->size > max_pool_size) { + LOG_DBG("memfd offset wrap around"); + if (!wrap_buffer(buf, 0)) + goto err; + } + + off_t new_offset = buf->offset + diff; + xassert(new_offset > buf->offset); + xassert(new_offset + buf->size <= max_pool_size); + +#if TIME_SCROLL + struct timespec tot; + struct timespec time1; + clock_gettime(CLOCK_MONOTONIC, &time1); + + struct timespec time2 = time1; +#endif + + if (top_keep_rows > 0) { + /* Copy current 'top' region to its new location */ + const int stride = buf->public.stride; + uint8_t *base = buf->public.data; + + memmove( + base + (top_margin + rows) * stride, + base + (top_margin + 0) * stride, + top_keep_rows * stride); + +#if TIME_SCROLL + clock_gettime(CLOCK_MONOTONIC, &time2); + timespec_sub(&time2, &time1, &tot); + LOG_INFO("memmove (top region): %lds %ldns", + (long)tot.tv_sec, tot.tv_nsec); +#endif + } + + /* Destroy old objects (they point to the old offset) */ + buffer_destroy_dont_close(&buf->public); + + /* Free unused memory - everything up until the new offset */ + const off_t trim_ofs = 0; + const off_t trim_len = new_offset; + + if (fallocate( + pool->fd, + FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, + trim_ofs, trim_len) < 0) + { + LOG_ERRNO("failed to trim SHM backing memory file"); + goto err; + } + +#if TIME_SCROLL + struct timespec time3; + clock_gettime(CLOCK_MONOTONIC, &time3); + timespec_sub(&time3, &time2, &tot); + LOG_INFO("PUNCH HOLE: %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); +#endif + + /* Re-instantiate pixman+wl_buffer+raw pointersw */ + bool ret = instantiate_offset(buf, new_offset); + +#if TIME_SCROLL + struct timespec time4; + clock_gettime(CLOCK_MONOTONIC, &time4); + timespec_sub(&time4, &time3, &tot); + LOG_INFO("instantiate offset: %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); +#endif + + if (ret && bottom_keep_rows > 0) { + /* Copy 'bottom' region to its new location */ + const size_t size = buf->size; + const int stride = buf->public.stride; + uint8_t *base = buf->public.data; + + memmove( + base + size - (bottom_margin + bottom_keep_rows) * stride, + base + size - (bottom_margin + rows + bottom_keep_rows) * stride, + bottom_keep_rows * stride); + +#if TIME_SCROLL + struct timespec time5; + clock_gettime(CLOCK_MONOTONIC, &time5); + + timespec_sub(&time5, &time4, &tot); + LOG_INFO("memmove (bottom region): %lds %ldns", + (long)tot.tv_sec, tot.tv_nsec); +#endif + } + + return ret; + +err: + abort(); + return false; +} + +static bool +shm_scroll_reverse(struct buffer_private *buf, int rows, + int top_margin, int top_keep_rows, + int bottom_margin, int bottom_keep_rows) +{ + xassert(rows > 0); + + struct buffer_pool *pool = buf->pool; + xassert(pool->ref_count == 1); + + const off_t diff = rows * buf->public.stride; + if (diff > buf->offset) { + LOG_DBG("memfd offset reverse wrap-around"); + if (!wrap_buffer(buf, (max_pool_size - buf->size) & ~(page_size() - 1))) + goto err; + } + + off_t new_offset = buf->offset - diff; + xassert(new_offset < buf->offset); + xassert(new_offset <= max_pool_size); + +#if TIME_SCROLL + struct timespec time0; + clock_gettime(CLOCK_MONOTONIC, &time0); + + struct timespec tot; + struct timespec time1 = time0; +#endif + + if (bottom_keep_rows > 0) { + /* Copy 'bottom' region to its new location */ + const size_t size = buf->size; + const int stride = buf->public.stride; + uint8_t *base = buf->public.data; + + memmove( + base + size - (bottom_margin + rows + bottom_keep_rows) * stride, + base + size - (bottom_margin + bottom_keep_rows) * stride, + bottom_keep_rows * stride); + +#if TIME_SCROLL + clock_gettime(CLOCK_MONOTONIC, &time1); + timespec_sub(&time1, &time0, &tot); + LOG_INFO("memmove (bottom region): %lds %ldns", + (long)tot.tv_sec, tot.tv_nsec); +#endif + } + + /* Destroy old objects (they point to the old offset) */ + buffer_destroy_dont_close(&buf->public); + + /* Free unused memory - everything after the relocated buffer */ + const off_t trim_ofs = new_offset + buf->size; + const off_t trim_len = pool->mmap_size - trim_ofs; + + if (fallocate( + pool->fd, + FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, + trim_ofs, trim_len) < 0) + { + LOG_ERRNO("failed to trim SHM backing memory"); + goto err; + } +#if TIME_SCROLL + struct timespec time2; + clock_gettime(CLOCK_MONOTONIC, &time2); + timespec_sub(&time2, &time1, &tot); + LOG_INFO("fallocate: %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); +#endif + + /* Re-instantiate pixman+wl_buffer+raw pointers */ + bool ret = instantiate_offset(buf, new_offset); + +#if TIME_SCROLL + struct timespec time3; + clock_gettime(CLOCK_MONOTONIC, &time3); + timespec_sub(&time3, &time2, &tot); + LOG_INFO("instantiate offset: %lds %ldns", (long)tot.tv_sec, tot.tv_nsec); +#endif + + if (ret && top_keep_rows > 0) { + /* Copy current 'top' region to its new location */ + const int stride = buf->public.stride; + uint8_t *base = buf->public.data; + + memmove( + base + (top_margin + 0) * stride, + base + (top_margin + rows) * stride, + top_keep_rows * stride); + +#if TIME_SCROLL + struct timespec time4; + clock_gettime(CLOCK_MONOTONIC, &time4); + timespec_sub(&time4, &time3, &tot); + LOG_INFO("memmove (top region): %lds %ldns", + (long)tot.tv_sec, tot.tv_nsec); +#endif + } + + return ret; + +err: + abort(); + return false; +} +#endif /* FALLOC_FL_PUNCH_HOLE */ + +bool +shm_scroll(struct buffer *_buf, int rows, + int top_margin, int top_keep_rows, + int bottom_margin, int bottom_keep_rows) +{ +#if __SIZEOF_POINTER__ == 8 && defined(FALLOC_FL_PUNCH_HOLE) + if (!shm_can_scroll(_buf)) + return false; + + struct buffer_private *buf = (struct buffer_private *)_buf; + + xassert(rows != 0); + return rows > 0 + ? shm_scroll_forward(buf, rows, top_margin, top_keep_rows, bottom_margin, bottom_keep_rows) + : shm_scroll_reverse(buf, -rows, top_margin, top_keep_rows, bottom_margin, bottom_keep_rows); +#else + return false; +#endif +} + +void +shm_purge(struct buffer_chain *chain) +{ + LOG_DBG("chain: %p: purging all buffers", (void *)chain); + + /* Purge old buffers associated with this cookie */ + tll_foreach(chain->bufs, it) { + if (buffer_unref_no_remove_from_chain(it->item)) + tll_remove(chain->bufs, it); + } +} + +void +shm_addref(struct buffer *_buf) +{ + struct buffer_private *buf = (struct buffer_private *)_buf; + buf->ref_count++; +} + +void +shm_unref(struct buffer *_buf) +{ + if (_buf == NULL) + return; + + struct buffer_private *buf = (struct buffer_private *)_buf; + struct buffer_chain *chain = buf->chain; + + tll_foreach(chain->bufs, it) { + if (it->item != buf) + continue; + + if (buffer_unref_no_remove_from_chain(buf)) + tll_remove(chain->bufs, it); + break; + } +} + +struct buffer_chain * +shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, + enum shm_bit_depth desired_bit_depth, + void (*release_cb)(struct buffer *buf, void *data), void *cb_data) +{ + pixman_format_code_t pixman_fmt = PIXMAN_a8r8g8b8; + enum wl_shm_format shm_fmt = WL_SHM_FORMAT_ARGB8888; + + static bool have_logged = false; + static bool have_logged_10_fallback = false; + +#if defined(HAVE_PIXMAN_RGBA_16) + static bool have_logged_16_fallback = false; + + if (desired_bit_depth == SHM_BITS_16) { + if (wayl->shm_have_abgr161616) { + pixman_fmt = PIXMAN_a16b16g16r16; + shm_fmt = WL_SHM_FORMAT_ABGR16161616; + + if (!have_logged) { + have_logged = true; + LOG_INFO("using 16-bit BGR surfaces"); + } + } else { + if (!have_logged_16_fallback) { + have_logged_16_fallback = true; + + LOG_WARN( + "16-bit surfaces requested, but compositor does not " + "implement ABGR161616+XBGR161616"); + } + } + } +#endif + + if (desired_bit_depth >= SHM_BITS_10 && pixman_fmt == PIXMAN_a8r8g8b8) { + if (wayl->shm_have_argb2101010) { + pixman_fmt = PIXMAN_a2r10g10b10; + shm_fmt = WL_SHM_FORMAT_ARGB2101010; + + if (!have_logged) { + have_logged = true; + LOG_INFO("using 10-bit RGB surfaces"); + } + } + + else if (wayl->shm_have_abgr2101010) { + pixman_fmt = PIXMAN_a2b10g10r10; + shm_fmt = WL_SHM_FORMAT_ABGR2101010; + + if (!have_logged) { + have_logged = true; + LOG_INFO("using 10-bit BGR surfaces"); + } + } + + else { + if (!have_logged_10_fallback) { + have_logged_10_fallback = true; + + LOG_WARN( + "10-bit surfaces requested, but compositor does not " + "implement ARGB2101010+XRGB2101010, or " + "ABGR2101010+XBGR2101010"); + } + } + } else { + if (!have_logged) { + have_logged = true; + LOG_INFO("using 8-bit RGB surfaces"); + } + } + + struct buffer_chain *chain = xmalloc(sizeof(*chain)); + *chain = (struct buffer_chain){ + .bufs = tll_init(), + .shm = wayl->shm, + .pix_instances = pix_instances, + .scrollable = scrollable, + + .pixman_fmt = pixman_fmt, + .shm_format = shm_fmt, + + .release_cb = release_cb, + .cb_data = cb_data, + }; + return chain; +} + +void +shm_chain_free(struct buffer_chain *chain) +{ + if (chain == NULL) + return; + + shm_purge(chain); + + if (tll_length(chain->bufs) > 0) { + BUG("chain=%p: there are buffers remaining; " + "is there a missing call to shm_unref()?", (void *)chain); + } + + free(chain); +} + +enum shm_bit_depth +shm_chain_bit_depth(const struct buffer_chain *chain) +{ + const pixman_format_code_t fmt = chain->pixman_fmt; + + return fmt == PIXMAN_a8r8g8b8 + ? SHM_BITS_8 +#if defined(HAVE_PIXMAN_RGBA_16) + : fmt == PIXMAN_a16b16g16r16 + ? SHM_BITS_16 +#endif + : SHM_BITS_10; +} diff --git a/shm.h b/shm.h new file mode 100644 index 0000000..c58a853 --- /dev/null +++ b/shm.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include + +#include "config.h" +#include "wayland.h" + +struct damage; + +struct buffer { + int width; + int height; + int stride; + + void *data; + + struct wl_buffer *wl_buf; + pixman_image_t **pix; + size_t pix_instances; + + unsigned age; + + /* + * First item in the array is used to track frame-to-frame + * damage. This is used when re-applying damage from the last + * frame, when the compositor doesn't release buffers immediately + * (forcing us to double buffer) + * + * The remaining items are used to track surface damage. Each + * worker thread adds its own cell damage to "its" region. When + * the frame is done, all damage is converted to a single region, + * which is then used in calls to wl_surface_damage_buffer(). + */ + pixman_region32_t *dirty; +}; + +void shm_fini(void); + +/* TODO: combine into shm_init() */ +void shm_set_max_pool_size(off_t max_pool_size); +void shm_set_min_stride_alignment(size_t min_stride_alignment); + +struct buffer_chain; +struct buffer_chain *shm_chain_new( + struct wayland *wayl, bool scrollable, size_t pix_instances, + enum shm_bit_depth desired_bit_depth, + void (*release_cb)(struct buffer *buf, void *data), void *cb_data); +void shm_chain_free(struct buffer_chain *chain); + +enum shm_bit_depth shm_chain_bit_depth(const struct buffer_chain *chain); + +/* + * Returns a single buffer. + * + * May returned a cached buffer. If so, the buffer's age indicates how + * many shm_get_buffer() calls have been made for the same + * width/height while the buffer was still busy. + * + * A newly allocated buffer has an age of 1234. + */ +struct buffer *shm_get_buffer(struct buffer_chain *chain, int width, int height); +/* + * Returns many buffers, described by 'info', all sharing the same SHM + * buffer pool. + * + * Never returns cached buffers. However, the newly created buffers + * are all inserted into the regular buffer cache, and are treated + * just like buffers created by shm_get_buffer(). + * + * This function is useful when allocating many small buffers, with + * (roughly) the same life time. + * + * Buffers are tagged for immediate purging, and will be destroyed as + * soon as the compositor releases them. + */ +void shm_get_many( + struct buffer_chain *chain, size_t count, + int widths[static count], int heights[static count], + struct buffer *bufs[static count]); + +void shm_did_not_use_buf(struct buffer *buf); + +bool shm_can_scroll(const struct buffer *buf); +bool shm_scroll(struct buffer *buf, int rows, + int top_margin, int top_keep_rows, + int bottom_margin, int bottom_keep_rows); + +void shm_addref(struct buffer *buf); +void shm_unref(struct buffer *buf); + +void shm_purge(struct buffer_chain *chain); diff --git a/sixel.c b/sixel.c new file mode 100644 index 0000000..187f134 --- /dev/null +++ b/sixel.c @@ -0,0 +1,2228 @@ +#include "sixel.h" + +#include +#include + +#define LOG_MODULE "sixel" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" +#include "grid.h" +#include "hsl.h" +#include "render.h" +#include "srgb.h" +#include "util.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +static size_t count; + +static void sixel_put_generic(struct terminal *term, uint8_t c); +static void sixel_put_ar_11(struct terminal *term, uint8_t c); + +static uint32_t +color_decode_srgb(const struct terminal *term, uint16_t r, uint16_t g, uint16_t b) +{ + if (term->sixel.linear_blending) { + if (term->sixel.use_10bit) { + r = srgb_decode_8_to_16(r) >> 6; + g = srgb_decode_8_to_16(g) >> 6; + b = srgb_decode_8_to_16(b) >> 6; + } else { + r = srgb_decode_8_to_8(r); + g = srgb_decode_8_to_8(g); + b = srgb_decode_8_to_8(b); + } + } else { + if (term->sixel.use_10bit) { + r <<= 2; + g <<= 2; + b <<= 2; + } + } + + uint32_t color; + + if (term->sixel.use_10bit) { + if (PIXMAN_FORMAT_TYPE(term->sixel.pixman_fmt) == PIXMAN_TYPE_ARGB) + color = 0x3u << 30 | r << 20 | g << 10 | b; + else + color = 0x3u << 30 | b << 20 | g << 10 | r; + } else + color = 0xffu << 24 | r << 16 | g << 8 | b; + + return color; +} + +void +sixel_fini(struct terminal *term) +{ + free(term->sixel.image.data); + free(term->sixel.private_palette); + free(term->sixel.shared_palette); +} + +sixel_put +sixel_init(struct terminal *term, int p1, int p2, int p3) +{ + /* + * P1: pixel aspect ratio + * - 0,1 - 2:1 + * - 2 - 5:1 + * - 3,4 - 3:1 + * - 5,6 - 2:1 + * - 7,8,9 - 1:1 + * + * P2: background color mode + * - 0|2: empty pixels use current background color + * - 1: empty pixels remain at their current color (i.e. transparent) + * P3: horizontal grid size - ignored + */ + + xassert(term->sixel.image.data == NULL); + xassert(term->sixel.palette_size <= SIXEL_MAX_COLORS); + + /* Default aspect ratio is 2:1 */ + const int pad = 1; + const int pan = + (p1 == 2) ? 5 : + (p1 == 3 || p1 == 4) ? 3 : + (p1 == 7 || p1 == 8 || p1 == 9) ? 1 : 2; + + LOG_DBG("initializing sixel with " + "p1=%d (pan=%d, pad=%d, aspect-ratio=%d:%d), " + "p2=%d (transparent=%s), " + "p3=%d (ignored)", + p1, pan, pad, pan, pad, p2, p2 == 1 ? "yes" : "no", p3); + + term->sixel.state = SIXEL_DECSIXEL; + term->sixel.pos = (struct coord){0, 0}; + term->sixel.color_idx = 0; + term->sixel.pan = pan; + term->sixel.pad = pad; + term->sixel.param = 0; + term->sixel.param_idx = 0; + memset(term->sixel.params, 0, sizeof(term->sixel.params)); + term->sixel.transparent_bg = p2 == 1; + term->sixel.image.data = NULL; + term->sixel.image.p = NULL; + term->sixel.image.width = 0; + term->sixel.image.height = 0; + term->sixel.image.alloc_height = 0; + term->sixel.image.bottom_pixel = 0; + term->sixel.linear_blending = wayl_do_linear_blending(term->wl, term->conf); + term->sixel.pixman_fmt = PIXMAN_a8r8g8b8; + + /* + * Use higher-precision sixel surfaces if we're using + * higher-precision window surfaces. + * + * This is to a) get more accurate colors when doing gamma-correct + * blending, and b) use the same pixman format as the main + * surfaces, for (hopefully) better performance. + * + * For now, don't support 16-bit surfaces (too much sixel logic + * that assumes 32-bit pixels). + */ + if (shm_chain_bit_depth(term->render.chains.grid) >= SHM_BITS_10) { + if (term->wl->shm_have_argb2101010) { + term->sixel.use_10bit = true; + term->sixel.pixman_fmt = PIXMAN_a2r10g10b10; + } + + else if (term->wl->shm_have_abgr2101010) { + term->sixel.use_10bit = true; + term->sixel.pixman_fmt = PIXMAN_a2b10g10r10; + } + } + + const size_t active_palette_entries = min( + ALEN(term->conf->colors_dark.sixel), term->sixel.palette_size); + + if (term->sixel.use_private_palette) { + xassert(term->sixel.private_palette == NULL); + term->sixel.private_palette = xcalloc( + term->sixel.palette_size, sizeof(term->sixel.private_palette[0])); + + memcpy( + term->sixel.private_palette, term->conf->colors_dark.sixel, + active_palette_entries * sizeof(term->sixel.private_palette[0])); + + if (term->sixel.linear_blending || term->sixel.use_10bit) { + for (size_t i = 0; i < active_palette_entries; i++) { + uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff; + uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff; + uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff; + term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b); + } + } + + term->sixel.palette = term->sixel.private_palette; + } else { + if (term->sixel.shared_palette == NULL) { + term->sixel.shared_palette = xcalloc( + term->sixel.palette_size, sizeof(term->sixel.shared_palette[0])); + + memcpy( + term->sixel.shared_palette, term->conf->colors_dark.sixel, + active_palette_entries * sizeof(term->sixel.shared_palette[0])); + + if (term->sixel.linear_blending || term->sixel.use_10bit) { + for (size_t i = 0; i < active_palette_entries; i++) { + uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff; + uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff; + uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff; + term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b); + } + } + } else { + /* Shared palette - do *not* reset palette for new sixels */ + } + + term->sixel.palette = term->sixel.shared_palette; + } + + count = 0; + return pan == 1 && pad == 1 ? &sixel_put_ar_11 : &sixel_put_generic; +} + +static void +sixel_invalidate_cache(struct sixel *sixel) +{ + if (sixel->scaled.pix != NULL) + pixman_image_unref(sixel->scaled.pix); + + free(sixel->scaled.data); + sixel->scaled.pix = NULL; + sixel->scaled.data = NULL; + sixel->scaled.width = -1; + sixel->scaled.height = -1; + + sixel->pix = NULL; + sixel->width = -1; + sixel->height = -1; +} + +void +sixel_destroy(struct sixel *sixel) +{ + sixel_invalidate_cache(sixel); + + if (sixel->original.pix != NULL) + pixman_image_unref(sixel->original.pix); + + free(sixel->original.data); + sixel->original.pix = NULL; + sixel->original.data = NULL; +} + +void +sixel_destroy_all(struct terminal *term) +{ + tll_foreach(term->normal.sixel_images, it) + sixel_destroy(&it->item); + tll_foreach(term->alt.sixel_images, it) + sixel_destroy(&it->item); + tll_free(term->normal.sixel_images); + tll_free(term->alt.sixel_images); +} + +static void +sixel_erase(struct terminal *term, struct sixel *sixel) +{ + for (int i = 0; i < sixel->rows; i++) { + int r = (sixel->pos.row + i) & (term->grid->num_rows - 1); + + struct row *row = term->grid->rows[r]; + if (row == NULL) { + /* A resize/reflow may cause row to now be unallocated */ + continue; + } + + row->dirty = true; + + for (int c = sixel->pos.col; c < min(sixel->pos.col + sixel->cols, term->cols); c++) + row->cells[c].attrs.clean = 0; + } + + sixel_destroy(sixel); +} + +/* + * Verify the sixels are sorted correctly. + * + * The sixels are sorted on their *end* row, in descending order. This + * invariant means the most recent sixels appear first in the list. + */ +static void +verify_list_order(const struct terminal *term) +{ +#if defined(_DEBUG) + int prev_row = INT_MAX; + int prev_col = -1; + int prev_col_count = 0; + + /* To aid debugging */ + size_t UNUSED idx = 0; + + tll_foreach(term->grid->sixel_images, it) { + int row = grid_row_abs_to_sb( + term->grid, term->rows, it->item.pos.row + it->item.rows - 1); + int col = it->item.pos.col; + int col_count = it->item.cols; + + xassert(row <= prev_row); + + if (row == prev_row) { + /* Allowed to be on the same row only if their columns + * don't overlap */ + + xassert(col + col_count <= prev_col || + prev_col + prev_col_count <= col); + } + + prev_row = row; + prev_col = col; + prev_col_count = col_count; + idx++; + } +#endif +} + +/* + * Verifies there aren't any sixels that cross the scrollback + * wrap-around. This invariant means a sixel's absolute row numbers + * are strictly increasing. + */ +static void +verify_no_wraparound_crossover(const struct terminal *term) +{ +#if defined(_DEBUG) + tll_foreach(term->grid->sixel_images, it) { + const struct sixel *six = &it->item; + + xassert(six->pos.row >= 0); + xassert(six->pos.row < term->grid->num_rows); + + int end = (six->pos.row + six->rows - 1) & (term->grid->num_rows - 1); + xassert(end >= six->pos.row); + } +#endif +} + +/* + * Verify there aren't any sixels that cross the scrollback end. This + * invariant means a sixel's rebased row numbers are strictly + * increasing. + */ +static void +verify_scrollback_consistency(const struct terminal *term) +{ +#if defined(_DEBUG) + tll_foreach(term->grid->sixel_images, it) { + const struct sixel *six = &it->item; + + int last_row = -1; + for (int i = 0; i < six->rows; i++) { + int row_no = grid_row_abs_to_sb( + term->grid, term->rows, six->pos.row + i); + + if (last_row != -1) + xassert(last_row < row_no); + + last_row = row_no; + } + } +#endif +} + +/* + * Verifies no sixel overlap with any other sixels. + */ +static void +verify_no_overlap(const struct terminal *term) +{ +#if defined(_DEBUG) + tll_foreach(term->grid->sixel_images, it) { + const struct sixel *six1 = &it->item; + + pixman_region32_t rect1; + pixman_region32_init_rect( + &rect1, six1->pos.col, six1->pos.row, six1->cols, six1->rows); + + tll_foreach(term->grid->sixel_images, it2) { + const struct sixel *six2 = &it2->item; + + if (six1 == six2) + continue; + + pixman_region32_t rect2; + pixman_region32_init_rect( + &rect2, six2->pos.col, + six2->pos.row, six2->cols, six2->rows); + + pixman_region32_t intersection; + pixman_region32_init(&intersection); + pixman_region32_intersect(&intersection, &rect1, &rect2); + + xassert(!pixman_region32_not_empty(&intersection)); + + pixman_region32_fini(&intersection); + pixman_region32_fini(&rect2); + } + + pixman_region32_fini(&rect1); + } +#endif +} + +static void +verify_sixels(const struct terminal *term) +{ + verify_no_wraparound_crossover(term); + verify_scrollback_consistency(term); + verify_no_overlap(term); + verify_list_order(term); +} + +static void +sixel_insert(struct terminal *term, struct sixel sixel) +{ + int end_row = grid_row_abs_to_sb( + term->grid, term->rows, sixel.pos.row + sixel.rows - 1); + + tll_foreach(term->grid->sixel_images, it) { + int rebased = grid_row_abs_to_sb( + term->grid, term->rows, it->item.pos.row + it->item.rows - 1); + + if (rebased < end_row) { + tll_insert_before(term->grid->sixel_images, it, sixel); + goto out; + } + } + + tll_push_back(term->grid->sixel_images, sixel); + +out: +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + LOG_DBG("sixel list after insertion:"); + tll_foreach(term->grid->sixel_images, it) { + LOG_DBG(" rows=%d+%d", it->item.pos.row, it->item.rows); + } +#endif + verify_sixels(term); +} + +void +sixel_scroll_up(struct terminal *term, int rows) +{ + if (likely(tll_length(term->grid->sixel_images) == 0)) + return; + + tll_rforeach(term->grid->sixel_images, it) { + struct sixel *six = &it->item; + + int six_start = grid_row_abs_to_sb(term->grid, term->rows, six->pos.row); + + if (six_start < rows) { + sixel_erase(term, six); + tll_remove(term->grid->sixel_images, it); + } else { + /* + * Unfortunately, we cannot break here. + * + * The sixels are sorted on their *end* row. This means + * there may be a sixel with a top row that will be + * scrolled out *anywhere* in the list (think of a huuuuge + * sixel that covers the entire scrollback) + */ + //break; + } + } + + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; + term_update_ascii_printer(term); + verify_sixels(term); +} + +void +sixel_scroll_down(struct terminal *term, int rows) +{ + if (likely(tll_length(term->grid->sixel_images) == 0)) + return; + + xassert(term->grid->num_rows >= rows); + + tll_foreach(term->grid->sixel_images, it) { + struct sixel *six = &it->item; + + int six_end = grid_row_abs_to_sb( + term->grid, term->rows, six->pos.row + six->rows - 1); + if (six_end >= term->grid->num_rows - rows) { + sixel_erase(term, six); + tll_remove(term->grid->sixel_images, it); + } else + break; + } + + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; + term_update_ascii_printer(term); + verify_sixels(term); +} + +static void +blend_new_image_over_old(const struct terminal *term, + const struct sixel *six, pixman_region32_t *six_rect, + int row, int col, pixman_image_t **pix, bool *opaque) +{ + xassert(pix != NULL); + xassert(opaque != NULL); + + /* + * TODO: handle images being emitted with different cell dimensions + */ + + const int six_ofs_x = six->pos.col * six->cell_width; + const int six_ofs_y = six->pos.row * six->cell_height; + const int img_ofs_x = col * six->cell_width; + const int img_ofs_y = row * six->cell_height; + const int img_width = pixman_image_get_width(*pix); + const int img_height = pixman_image_get_height(*pix); + + pixman_region32_t pix_rect; + pixman_region32_init_rect( + &pix_rect, img_ofs_x, img_ofs_y, img_width, img_height); + + /* Blend the intersection between the old and new images */ + pixman_region32_t intersection; + pixman_region32_init(&intersection); + pixman_region32_intersect(&intersection, six_rect, &pix_rect); + + int n_rects = -1; + pixman_box32_t *boxes = pixman_region32_rectangles( + &intersection, &n_rects); + + if (n_rects == 0) + goto out; + + xassert(n_rects == 1); + pixman_box32_t *box = &boxes[0]; + + if (!*opaque) { + /* + * New image is transparent - blend on top of the old + * sixel image. + */ + pixman_image_composite32( + PIXMAN_OP_OVER_REVERSE, + six->original.pix, NULL, *pix, + box->x1 - six_ofs_x, box->y1 - six_ofs_y, + 0, 0, + box->x1 - img_ofs_x, box->y1 - img_ofs_y, + box->x2 - box->x1, box->y2 - box->y1); + } + + /* + * Since the old image is split into sub-tiles on a + * per-row basis, we need to enlarge the new image and + * copy the old image if the old image extends beyond the + * new image. + * + * The "bounding" coordinates are either the edges of the + * old image, or the next cell boundary, whichever comes + * first. + */ + int bounding_x = six_ofs_x + six->original.width > img_ofs_x + img_width + ? min( + six_ofs_x + six->original.width, + (box->x2 + six->cell_width - 1) / six->cell_width * six->cell_width) + : box->x2; + int bounding_y = six_ofs_y + six->original.height > img_ofs_y + img_height + ? min( + six_ofs_y + six->original.height, + (box->y2 + six->cell_height - 1) / six->cell_height * six->cell_height) + : box->y2; + + /* The required size of the new image */ + const int required_width = bounding_x - img_ofs_x; + const int required_height = bounding_y - img_ofs_y; + + const int new_width = max(img_width, required_width); + const int new_height = max(img_height, required_height); + + if (new_width <= img_width && new_height <= img_height) + goto out; + + //LOG_INFO("enlarging: %dx%d -> %dx%d", img_width, img_height, new_width, new_height); + + if (!six->opaque) { + /* Transparency is viral */ + *opaque = false; + } + + /* Create a new pixmap */ + int stride = new_width * sizeof(uint32_t); + uint32_t *new_data = xmalloc(stride * new_height); + pixman_image_t *pix2 = pixman_image_create_bits_no_clear( + term->sixel.pixman_fmt, new_width, new_height, new_data, stride); + +#if defined(_DEBUG) + /* Fill new image with an easy-to-recognize color (green) */ + for (size_t i = 0; i < new_width * new_height; i++) + new_data[i] = 0xff00ff00; +#endif + + /* Copy the new image, from its old pixmap, to the new pixmap */ + pixman_image_composite32( + PIXMAN_OP_SRC, + *pix, NULL, pix2, 0, 0, 0, 0, 0, 0, img_width, img_height); + + /* Copy the bottom tile of the old sixel image into the new pixmap */ + pixman_image_composite32( + PIXMAN_OP_SRC, + six->original.pix, NULL, pix2, + box->x1 - six_ofs_x, box->y2 - six_ofs_y, + 0, 0, + box->x1 - img_ofs_x, box->y2 - img_ofs_y, + bounding_x - box->x1, bounding_y - box->y2); + + /* Copy the right tile of the old sixel image into the new pixmap */ + pixman_image_composite32( + PIXMAN_OP_SRC, + six->original.pix, NULL, pix2, + box->x2 - six_ofs_x, box->y1 - six_ofs_y, + 0, 0, + box->x2 - img_ofs_x, box->y1 - img_ofs_y, + bounding_x - box->x2, bounding_y - box->y1); + + /* + * Ensure the newly allocated area is initialized. + * + * Some of it, or all, will have been initialized above, by the + * bottom and right tiles from the old sixel image. However, there + * may be areas in the new image that isn't covered by the old + * image. These areas need to be made transparent. + */ + pixman_region32_t uninitialized; + pixman_region32_init_rects( + &uninitialized, + (const pixman_box32_t []){ + /* Extended image area on the right side */ + {img_ofs_x + img_width, img_ofs_y, img_ofs_x + new_width, img_ofs_y + new_height}, + + /* Bottom */ + {img_ofs_x, img_ofs_y + img_height, img_ofs_x + new_width, img_ofs_y + new_height}}, + 2); + + /* Subtract the old sixel image, since the area(s) covered by the + * old image has already been copied, and *must* not be + * overwritten */ + pixman_region32_t diff; + pixman_region32_init(&diff); + pixman_region32_subtract(&diff, &uninitialized, six_rect); + + if (pixman_region32_not_empty(&diff)) { + pixman_image_t *src = + pixman_image_create_solid_fill(&(pixman_color_t){0}); + + int count = -1; + pixman_box32_t *rects = pixman_region32_rectangles(&diff, &count); + + for (int i = 0; i < count; i++) { + pixman_image_composite32( + PIXMAN_OP_SRC, + src, NULL, pix2, + 0, 0, 0, 0, + rects[i].x1 - img_ofs_x, rects[i].y1 - img_ofs_y, + rects[i].x2 - rects[i].x1, + rects[i].y2 - rects[i].y1); + } + + pixman_image_unref(src); + *opaque = false; + } + + pixman_region32_fini(&diff); + pixman_region32_fini(&uninitialized); + + /* Use the new pixmap in place of the old one */ + free(pixman_image_get_data(*pix)); + pixman_image_unref(*pix); + *pix = pix2; + +out: + pixman_region32_fini(&intersection); + pixman_region32_fini(&pix_rect); +} + +static void +sixel_overwrite(struct terminal *term, struct sixel *six, + int row, int col, int height, int width, + pixman_image_t **pix, bool *opaque) +{ + pixman_region32_t six_rect; + pixman_region32_init_rect( + &six_rect, + six->pos.col * six->cell_width, six->pos.row * six->cell_height, + six->original.width, six->original.height); + + pixman_region32_t overwrite_rect; + pixman_region32_init_rect( + &overwrite_rect, + col * six->cell_width, row * six->cell_height, + width * six->cell_width, height * six->cell_height); + +#if defined(_DEBUG) + pixman_region32_t cell_intersection; + pixman_region32_init(&cell_intersection); + pixman_region32_intersect(&cell_intersection, &six_rect, &overwrite_rect); + xassert(!pixman_region32_not_empty(&six_rect) || + pixman_region32_not_empty(&cell_intersection)); + pixman_region32_fini(&cell_intersection); +#endif + + if (pix != NULL) + blend_new_image_over_old(term, six, &six_rect, row, col, pix, opaque); + + pixman_region32_t diff; + pixman_region32_init(&diff); + pixman_region32_subtract(&diff, &six_rect, &overwrite_rect); + + pixman_region32_fini(&six_rect); + pixman_region32_fini(&overwrite_rect); + + int n_rects = -1; + pixman_box32_t *boxes = pixman_region32_rectangles(&diff, &n_rects); + + for (int i = 0; i < n_rects; i++) { + LOG_DBG("box #%d: x1=%d, y1=%d, x2=%d, y2=%d", i, + boxes[i].x1, boxes[i].y1, boxes[i].x2, boxes[i].y2); + + xassert(boxes[i].x1 % six->cell_width == 0); + xassert(boxes[i].y1 % six->cell_height == 0); + + /* New image's position, in cells */ + const int new_col = boxes[i].x1 / six->cell_width; + const int new_row = boxes[i].y1 / six->cell_height; + + xassert(new_row < term->grid->num_rows); + + /* New image's width and height, in pixels */ + const int new_width = boxes[i].x2 - boxes[i].x1; + const int new_height = boxes[i].y2 - boxes[i].y1; + + uint32_t *new_data = xmalloc(new_width * new_height * sizeof(uint32_t)); + const uint32_t *old_data = six->original.data; + + /* Pixel offsets into old image backing memory */ + const int x_ofs = boxes[i].x1 - six->pos.col * six->cell_width; + const int y_ofs = boxes[i].y1 - six->pos.row * six->cell_height; + + /* Copy image data, one row at a time */ + for (size_t j = 0; j < new_height; j++) { + memcpy( + &new_data[(0 + j) * new_width], + &old_data[(y_ofs + j) * six->original.width + x_ofs], + new_width * sizeof(uint32_t)); + } + + pixman_image_t *new_pix = pixman_image_create_bits_no_clear( + term->sixel.pixman_fmt, new_width, new_height, new_data, new_width * sizeof(uint32_t)); + + struct sixel new_six = { + .pix = NULL, + .width = -1, + .height = -1, + .pos = {.col = new_col, .row = new_row}, + .cols = (new_width + six->cell_width - 1) / six->cell_width, + .rows = (new_height + six->cell_height - 1) / six->cell_height, + .opaque = six->opaque, + .cell_width = six->cell_width, + .cell_height = six->cell_height, + .original = { + .data = new_data, + .pix = new_pix, + .width = new_width, + .height = new_height, + }, + .scaled = { + .data = NULL, + .pix = NULL, + .width = -1, + .height = -1, + }, + }; + +#if defined(_DEBUG) + /* Assert we don't cross the scrollback wrap-around */ + const int new_end = new_six.pos.row + new_six.rows - 1; + xassert(new_end < term->grid->num_rows); +#endif + + sixel_insert(term, new_six); + } + + pixman_region32_fini(&diff); +} + +/* Row numbers are absolute */ +static void +_sixel_overwrite_by_rectangle( + struct terminal *term, int row, int col, int height, int width, + pixman_image_t **pix, bool *opaque) +{ + verify_sixels(term); + +#if defined(_DEBUG) + pixman_region32_t overwrite_rect; + pixman_region32_init_rect(&overwrite_rect, col, row, width, height); +#endif + + const int start = row; + const int end = row + height - 1; + + /* We should never generate scrollback wrapping sixels */ + xassert(end < term->grid->num_rows); + + const int scrollback_rel_start = grid_row_abs_to_sb( + term->grid, term->rows, start); + + bool UNUSED would_have_breaked = false; + + tll_foreach(term->grid->sixel_images, it) { + struct sixel *six = &it->item; + + const int six_start = six->pos.row; + const int six_end = (six_start + six->rows - 1); + const int six_scrollback_rel_end = + grid_row_abs_to_sb(term->grid, term->rows, six_end); + + /* We should never generate scrollback wrapping sixels */ + xassert(six_end < term->grid->num_rows); + + if (six_scrollback_rel_end < scrollback_rel_start) { + /* All remaining sixels are *before* our rectangle */ + would_have_breaked = true; + break; + } + +#if defined(_DEBUG) + pixman_region32_t six_rect; + pixman_region32_init_rect(&six_rect, six->pos.col, six->pos.row, six->cols, six->rows); + + pixman_region32_t intersection; + pixman_region32_init(&intersection); + pixman_region32_intersect(&intersection, &six_rect, &overwrite_rect); + + const bool collides = pixman_region32_not_empty(&intersection); +#else + const bool UNUSED collides = false; +#endif + + if ((start <= six_start && end >= six_start) || /* Crosses sixel start boundary */ + (start <= six_end && end >= six_end) || /* Crosses sixel end boundary */ + (start >= six_start && end <= six_end)) /* Fully within sixel range */ + { + const int col_start = six->pos.col; + const int col_end = six->pos.col + six->cols - 1; + + if ((col <= col_start && col + width - 1 >= col_start) || + (col <= col_end && col + width - 1 >= col_end) || + (col >= col_start && col + width - 1 <= col_end)) + { + xassert(!would_have_breaked); + + struct sixel to_be_erased = *six; + tll_remove(term->grid->sixel_images, it); + + sixel_overwrite(term, &to_be_erased, start, col, height, width, + pix, opaque); + sixel_erase(term, &to_be_erased); + } else + xassert(!collides); + } else + xassert(!collides); + +#if defined(_DEBUG) + pixman_region32_fini(&intersection); + pixman_region32_fini(&six_rect); +#endif + } + +#if defined(_DEBUG) + pixman_region32_fini(&overwrite_rect); +#endif +} + +void +sixel_overwrite_by_rectangle( + struct terminal *term, int row, int col, int height, int width) +{ + if (likely(tll_length(term->grid->sixel_images) == 0)) + return; + + const int start = (term->grid->offset + row) & (term->grid->num_rows - 1); + const int end = (start + height - 1) & (term->grid->num_rows - 1); + const bool wraps = end < start; + + if (wraps) { + int rows_to_wrap_around = term->grid->num_rows - start; + xassert(height - rows_to_wrap_around > 0); + _sixel_overwrite_by_rectangle(term, start, col, rows_to_wrap_around, width, NULL, NULL); + _sixel_overwrite_by_rectangle(term, 0, col, height - rows_to_wrap_around, width, NULL, NULL); + } else + _sixel_overwrite_by_rectangle(term, start, col, height, width, NULL, NULL); + + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; + term_update_ascii_printer(term); +} + +/* Row numbers are relative to grid offset */ +void +sixel_overwrite_by_row(struct terminal *term, int _row, int col, int width) +{ + xassert(col >= 0); + + xassert(_row >= 0); + xassert(_row < term->rows); + xassert(col >= 0); + xassert(col < term->grid->num_cols); + + if (likely(tll_length(term->grid->sixel_images) == 0)) + return; + + if (col + width > term->grid->num_cols) + width = term->grid->num_cols - col; + + const int row = (term->grid->offset + _row) & (term->grid->num_rows - 1); + const int scrollback_rel_row = grid_row_abs_to_sb(term->grid, term->rows, row); + + tll_foreach(term->grid->sixel_images, it) { + struct sixel *six = &it->item; + const int six_start = six->pos.row; + const int six_end = (six_start + six->rows - 1) & (term->grid->num_rows - 1); + + /* We should never generate scrollback wrapping sixels */ + xassert(six_end >= six_start); + + const int six_scrollback_rel_end = + grid_row_abs_to_sb(term->grid, term->rows, six_end); + + if (six_scrollback_rel_end < scrollback_rel_row) { + /* All remaining sixels are *before* "our" row */ + break; + } + + if (row >= six_start && row <= six_end) { + const int col_start = six->pos.col; + const int col_end = six->pos.col + six->cols - 1; + + if ((col <= col_start && col + width - 1 >= col_start) || + (col <= col_end && col + width - 1 >= col_end) || + (col >= col_start && col + width - 1 <= col_end)) + { + struct sixel to_be_erased = *six; + tll_remove(term->grid->sixel_images, it); + + sixel_overwrite(term, &to_be_erased, row, col, 1, width, NULL, NULL); + sixel_erase(term, &to_be_erased); + } + } + } + + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; + term_update_ascii_printer(term); +} + +void +sixel_overwrite_at_cursor(struct terminal *term, int width) +{ + if (likely(tll_length(term->grid->sixel_images) == 0)) + return; + + sixel_overwrite_by_row( + term, term->grid->cursor.point.row, term->grid->cursor.point.col, width); +} + +void +sixel_cell_size_changed(struct terminal *term) +{ + tll_foreach(term->normal.sixel_images, it) + sixel_invalidate_cache(&it->item); + + tll_foreach(term->alt.sixel_images, it) + sixel_invalidate_cache(&it->item); +} + +void +sixel_sync_cache(const struct terminal *term, struct sixel *six) +{ + if (six->pix != NULL) { +#if defined(_DEBUG) + if (six->cell_width == term->cell_width && + six->cell_height == term->cell_height) + { + xassert(six->pix == six->original.pix); + xassert(six->width == six->original.width); + xassert(six->height == six->original.height); + + xassert(six->scaled.data == NULL); + xassert(six->scaled.pix == NULL); + xassert(six->scaled.width < 0); + xassert(six->scaled.height < 0); + } else { + xassert(six->pix == six->scaled.pix); + xassert(six->width == six->scaled.width); + xassert(six->height == six->scaled.height); + + xassert(six->scaled.data != NULL); + xassert(six->scaled.pix != NULL); + + /* TODO: check ratio */ + xassert(six->scaled.width >= 0); + xassert(six->scaled.height >= 0); + } +#endif + return; + } + + /* Cache should be invalid */ + xassert(six->scaled.data == NULL); + xassert(six->scaled.pix == NULL); + xassert(six->scaled.width < 0); + xassert(six->scaled.height < 0); + + if (six->cell_width == term->cell_width && + six->cell_height == term->cell_height) + { + six->pix = six->original.pix; + six->width = six->original.width; + six->height = six->original.height; + } else { + const double width_ratio = (double)term->cell_width / six->cell_width; + const double height_ratio = (double)term->cell_height / six->cell_height; + + struct pixman_f_transform scale; + pixman_f_transform_init_scale( + &scale, 1. / width_ratio, 1. / height_ratio); + + struct pixman_transform _scale; + pixman_transform_from_pixman_f_transform(&_scale, &scale); + pixman_image_set_transform(six->original.pix, &_scale); + pixman_image_set_filter(six->original.pix, PIXMAN_FILTER_BILINEAR, NULL, 0); + + int scaled_width = (double)six->original.width * width_ratio; + int scaled_height = (double)six->original.height * height_ratio; + int scaled_stride = scaled_width * sizeof(uint32_t); + + LOG_DBG("scaling sixel: %dx%d -> %dx%d", + six->original.width, six->original.height, + scaled_width, scaled_height); + + uint8_t *scaled_data = xmalloc(scaled_height * scaled_stride); + pixman_image_t *scaled_pix = pixman_image_create_bits_no_clear( + term->sixel.pixman_fmt, scaled_width, scaled_height, + (uint32_t *)scaled_data, scaled_stride); + + pixman_image_composite32( + PIXMAN_OP_SRC, six->original.pix, NULL, scaled_pix, 0, 0, 0, 0, + 0, 0, scaled_width, scaled_height); + + pixman_image_set_transform(six->original.pix, NULL); + + six->scaled.data = scaled_data; + six->scaled.pix = six->pix = scaled_pix; + six->scaled.width = six->width = scaled_width; + six->scaled.height = six->height = scaled_height; + } +} + +void +sixel_reflow_grid(struct terminal *term, struct grid *grid) +{ + /* Meh - the sixel functions we call use term->grid... */ + struct grid *active_grid = term->grid; + term->grid = grid; + + /* Need the "real" list to be empty from the beginning */ + tll(struct sixel) copy = tll_init(); + tll_foreach(grid->sixel_images, it) + tll_push_back(copy, it->item); + tll_free(grid->sixel_images); + + tll_rforeach(copy, it) { + struct sixel *six = &it->item; + int start = six->pos.row; + int end = (start + six->rows - 1) & (grid->num_rows - 1); + + if (end < start) { + /* Crosses scrollback wrap-around */ + /* TODO: split image */ + sixel_destroy(six); + continue; + } + + if (six->rows > grid->num_rows) { + /* Image too large */ + /* TODO: keep bottom part? */ + sixel_destroy(six); + continue; + } + + /* Drop sixels that now cross the current scrollback end + * border. This is similar to a sixel that have been + * scrolled out */ + /* TODO: should be possible to optimize this */ + bool sixel_destroyed = false; + int last_row = -1; + + for (int j = 0; j < six->rows; j++) { + int row_no = grid_row_abs_to_sb( + term->grid, term->rows, six->pos.row + j); + if (last_row != -1 && last_row >= row_no) { + sixel_destroy(six); + sixel_destroyed = true; + break; + } + + last_row = row_no; + } + + if (sixel_destroyed) { + LOG_WARN("destroyed sixel that now crossed history"); + continue; + } + + /* Sixels that didn't overlap may now do so, which isn't + * allowed of course */ + _sixel_overwrite_by_rectangle( + term, six->pos.row, six->pos.col, six->rows, six->cols, + &it->item.original.pix, &it->item.opaque); + + if (it->item.original.data != pixman_image_get_data(it->item.original.pix)) { + it->item.original.data = pixman_image_get_data(it->item.original.pix); + it->item.original.width = pixman_image_get_width(it->item.original.pix); + it->item.original.height = pixman_image_get_height(it->item.original.pix); + it->item.cols = (it->item.original.width + it->item.cell_width - 1) / it->item.cell_width; + it->item.rows = (it->item.original.height + it->item.cell_height - 1) / it->item.cell_height; + sixel_invalidate_cache(&it->item); + } + + sixel_insert(term, it->item); + } + + tll_free(copy); + term->grid = active_grid; +} + +void +sixel_reflow(struct terminal *term) +{ + for (size_t i = 0; i < 2; i++) { + struct grid *grid = i == 0 ? &term->normal : &term->alt; + sixel_reflow_grid(term, grid); + } +} + +void +sixel_unhook(struct terminal *term) +{ + if (term->sixel.pos.row < term->sixel.image.height && + term->sixel.pos.row + 6 * term->sixel.pan >= term->sixel.image.height) + { + /* + * Handle case where image has had its size set by raster + * attributes, and then one or more sixels were printed on the + * last row of the RA area. + * + * In this case, the image height may not be a multiple of + * 6*pan. But the printed sixels may still be outside the RA + * area. In this case, using the size from the RA would + * truncate the image. + * + * So, extend the image to a multiple of 6*pan. + * + * If this is a transparent image, the image may get trimmed + * below (most likely back the size set by RA). + */ + term->sixel.image.height = term->sixel.image.alloc_height; + } + + /* Strip trailing fully transparent rows, *unless* we *ended* with + * a trailing GNL, in which case we do *not* want to strip all 6 + * pixel rows */ + if (term->sixel.pos.col > 0) { + const int bits = sizeof(term->sixel.image.bottom_pixel) * 8; + const int leading_zeroes = term->sixel.image.bottom_pixel == 0 + ? bits + : __builtin_clz(term->sixel.image.bottom_pixel); + const int rows_to_trim = leading_zeroes + 6 - bits; + + LOG_DBG("bottom-pixel: 0x%02x, bits=%d, leading-zeroes=%d, " + "rows-to-trim=%d*%d", term->sixel.image.bottom_pixel, + bits, leading_zeroes, rows_to_trim, term->sixel.pan); + + /* + * If the current graphical cursor position is at the last row + * of the image, *and* the image is transparent (P2=1), trim + * the entire image. + * + * If the image is not transparent, then we can't trim the RA + * region (it is supposed to "erase", with the current + * background color.) + * + * We *do* "trim" transparent rows from the graphical cursor + * position, as this affects the positioning of the text + * cursor. + * + * See https://raw.githubusercontent.com/hackerb9/vt340test/main/sixeltests/p2effect.sh + */ + if (term->sixel.pos.row + 6 * term->sixel.pan >= term->sixel.image.alloc_height) { + LOG_DBG("trimming image"); + const int trimmed_height = + term->sixel.image.alloc_height - rows_to_trim * term->sixel.pan; + + if (term->sixel.transparent_bg) { + /* Image is transparent - trim as much as possible */ + term->sixel.image.height = trimmed_height; + } else { + /* Image is opaque. We can't trim anything "inside" + the RA region */ + if (trimmed_height > term->sixel.image.height) { + /* There are non-empty pixels *outside* the RA + region - trim up to that point */ + term->sixel.image.height = trimmed_height; + } + } + } else { + LOG_DBG("only adjusting cursor position"); + } + + term->sixel.pos.row += 6 * term->sixel.pan; + term->sixel.pos.row -= rows_to_trim * term->sixel.pan; + } + + int pixel_row_idx = 0; + int pixel_rows_left = term->sixel.image.height; + const int stride = term->sixel.image.width * sizeof(uint32_t); + + /* + * When sixel scrolling is enabled (the default), sixels behave + * pretty much like normal output; the sixel starts at the current + * cursor position and the cursor is moved to a point after the + * sixel. + * + * Furthermore, if the sixel reaches the bottom of the scrolling + * region, the terminal content is scrolled. + * + * When scrolling is disabled, sixels always start at (0,0), the + * cursor is not moved at all, and the terminal content never + * scrolls. + */ + + const bool do_scroll = term->sixel.scrolling; + + /* Number of rows we're allowed to use. + * + * When scrolling is enabled, we always allow the entire sixel to + * be emitted. + * + * When disabled, only the number of screen rows may be used. */ + int rows_avail = do_scroll + ? (term->sixel.image.height + term->cell_height - 1) / term->cell_height + : term->scroll_region.end; + + /* Initial sixel coordinates */ + int start_row = do_scroll ? term->grid->cursor.point.row : 0; + const int start_col = do_scroll ? term->grid->cursor.point.col : 0; + + /* Total number of rows needed by image */ + const int rows_needed = + (term->sixel.image.height + term->cell_height - 1) / term->cell_height; + + bool free_image_data = true; + + /* We do not allow sixels to cross the scrollback wrap-around, as + * this makes intersection calculations much more complicated */ + while (pixel_rows_left > 0 && + rows_avail > 0 && + rows_needed <= term->grid->num_rows) + { + const int cur_row = (term->grid->offset + start_row) & (term->grid->num_rows - 1); + const int rows_left_until_wrap_around = term->grid->num_rows - cur_row; + const int usable_rows = min(rows_avail, rows_left_until_wrap_around); + + const int pixel_rows_avail = usable_rows * term->cell_height; + + const int width = term->sixel.image.width; + const int height = min(pixel_rows_left, pixel_rows_avail); + + uint32_t *img_data; + if (pixel_row_idx == 0 && height == pixel_rows_left) { + /* Entire image will be emitted as a single chunk - reuse + * the source buffer */ + img_data = term->sixel.image.data; + free_image_data = false; + } else { + xassert(free_image_data); + img_data = xmalloc(height * stride); + memcpy( + img_data, + &((uint8_t *)term->sixel.image.data)[pixel_row_idx * stride], + height * stride); + } + + struct sixel image = { + .pix = NULL, + .width = -1, + .height = -1, + .rows = (height + term->cell_height - 1) / term->cell_height, + .cols = (width + term->cell_width - 1) / term->cell_width, + .pos = (struct coord){start_col, cur_row}, + .opaque = !term->sixel.transparent_bg, + .cell_width = term->cell_width, + .cell_height = term->cell_height, + .original = { + .data = img_data, + .pix = NULL, + .width = width, + .height = height, + }, + .scaled = { + .data = NULL, + .pix = NULL, + .width = -1, + .height = -1, + }, + }; + + xassert(image.rows <= term->grid->num_rows); + xassert(image.pos.row + image.rows - 1 < term->grid->num_rows); + + LOG_DBG("generating %s %dx%d pixman image at %d-%d", + image.opaque ? "opaque" : "transparent", + image.original.width, image.original.height, + image.pos.row, image.pos.row + image.rows); + + image.original.pix = pixman_image_create_bits_no_clear( + term->sixel.pixman_fmt, image.original.width, image.original.height, + img_data, stride); + + pixel_row_idx += height; + pixel_rows_left -= height; + rows_avail -= image.rows; + + if (do_scroll) { + /* + * Linefeeds - always one less than the number of rows + * occupied by the image. + * + * Unless this is *not* the last chunk. In that case, + * linefeed past the chunk, so that the next chunk + * "starts" at a "new" row. + */ + const int linefeed_count = rows_avail == 0 + ? max(0, image.rows - 1) + : image.rows; + + xassert(rows_avail == 0 || + image.original.height % term->cell_height == 0); + + for (size_t i = 0; i < linefeed_count; i++) + term_linefeed(term); + + /* Position text cursor if this is the last image chunk */ + if (rows_avail == 0) { + int row = term->grid->cursor.point.row; + + /* + * Position the text cursor based on the text row + * touched by the last sixel + */ + const int pixel_rows = pixel_rows_left > 0 + ? image.original.height + : term->sixel.pos.row; + const int term_rows = + (pixel_rows + term->cell_height - 1) / term->cell_height; + + xassert(term_rows <= image.rows); + + row -= (image.rows - term_rows); + + term_cursor_to( + term, + max(0, row), + (term->sixel.cursor_right_of_graphics + ? min(image.pos.col + image.cols, term->cols - 1) + : image.pos.col)); + } + + term->sixel.pos.row -= image.original.height; + } + + /* Dirty touched cells, and scroll terminal content if necessary */ + for (size_t i = 0; i < image.rows; i++) { + struct row *row = term->grid->rows[cur_row + i]; + row->dirty = true; + + for (int col = image.pos.col; + col < min(image.pos.col + image.cols, term->cols); + col++) + { + row->cells[col].attrs.clean = 0; + } + + } + + _sixel_overwrite_by_rectangle( + term, image.pos.row, image.pos.col, image.rows, image.cols, + &image.original.pix, &image.opaque); + + if (image.original.data != pixman_image_get_data(image.original.pix)) { + image.original.data = pixman_image_get_data(image.original.pix); + image.original.width = pixman_image_get_width(image.original.pix); + image.original.height = pixman_image_get_height(image.original.pix); + image.cols = (image.original.width + image.cell_width - 1) / image.cell_width; + image.rows = (image.original.height + image.cell_height - 1) / image.cell_height; + sixel_invalidate_cache(&image); + } + + sixel_insert(term, image); + + if (do_scroll) + start_row = term->grid->cursor.point.row; + else + start_row -= image.rows; + } + + if (free_image_data) + free(term->sixel.image.data); + + term->sixel.image.data = NULL; + term->sixel.image.p = NULL; + term->sixel.image.width = 0; + term->sixel.image.height = 0; + term->sixel.pos = (struct coord){0, 0}; + + free(term->sixel.private_palette); + term->sixel.private_palette = NULL; + + LOG_DBG("you now have %zu sixels in current grid", + tll_length(term->grid->sixel_images)); + + + term->bits_affecting_ascii_printer.sixels = + tll_length(term->grid->sixel_images) > 0; + term_update_ascii_printer(term); + render_refresh(term); +} + +static void ALWAYS_INLINE inline +memset_u32(uint32_t *data, uint32_t value, size_t count) +{ + static_assert(sizeof(wchar_t) == 4, "wchar_t is not 4 bytes"); + wmemset((wchar_t *)data, (wchar_t)value, count); +} + +static void +resize_horizontally(struct terminal *term, int new_width_mutable) +{ + if (unlikely(new_width_mutable > term->sixel.max_width)) { + LOG_WARN("maximum image dimensions exceeded, truncating"); + new_width_mutable = term->sixel.max_width; + } + + if (unlikely(term->sixel.image.width >= new_width_mutable)) + return; + + const int sixel_row_height = 6 * term->sixel.pan; + + uint32_t *old_data = term->sixel.image.data; + const int old_width = term->sixel.image.width; + const int new_width = new_width_mutable; + + int height; + if (unlikely(term->sixel.image.height == 0)) { + /* Lazy initialize height on first printed sixel */ + xassert(old_width == 0); + term->sixel.image.height = height = sixel_row_height; + term->sixel.image.alloc_height = sixel_row_height; + } else + height = term->sixel.image.height; + + LOG_DBG("resizing image horizontally: %dx(%d) -> %dx(%d)", + term->sixel.image.width, term->sixel.image.height, + new_width, height); + + int alloc_height = (height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; + + xassert(new_width >= old_width); + xassert(new_width > 0); + xassert(alloc_height > 0); + + /* Width (and thus stride) change - need to allocate a new buffer */ + uint32_t *new_data = xmalloc(new_width * alloc_height * sizeof(uint32_t)); + + uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; + + /* Copy old rows, and initialize new columns to background color */ + const uint32_t *end = &new_data[alloc_height * new_width]; + for (uint32_t *n = new_data, *o = old_data; + n < end; + n += new_width, o += old_width) + { + memcpy(n, o, old_width * sizeof(uint32_t)); + memset_u32(&n[old_width], bg, new_width - old_width); + } + + free(old_data); + + term->sixel.image.data = new_data; + term->sixel.image.width = new_width; + + const int ofs = term->sixel.pos.row * new_width + term->sixel.pos.col; + term->sixel.image.p = &term->sixel.image.data[ofs]; +} + +static bool +resize_vertically(struct terminal *term, const int new_height) +{ + LOG_DBG("resizing image vertically: (%d)x%d -> (%d)x%d", + term->sixel.image.width, term->sixel.image.height, + term->sixel.image.width, new_height); + + if (unlikely(new_height > term->sixel.max_height)) { + LOG_WARN("maximum image dimensions reached"); + return false; + } + + uint32_t *old_data = term->sixel.image.data; + const int width = term->sixel.image.width; + const int old_height = term->sixel.image.height; + const int sixel_row_height = 6 * term->sixel.pan; + + int alloc_height = (new_height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; + + xassert(new_height > 0); + + if (unlikely(width == 0)) { + xassert(term->sixel.image.data == NULL); + term->sixel.image.height = new_height; + term->sixel.image.alloc_height = alloc_height; + return true; + } + + uint32_t *new_data = realloc( + old_data, width * alloc_height * sizeof(uint32_t)); + + if (new_data == NULL) { + LOG_ERRNO("failed to reallocate sixel image buffer"); + return false; + } + + const uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; + + memset_u32(&new_data[old_height * width], + bg, + (alloc_height - old_height) * width); + + term->sixel.image.height = new_height; + term->sixel.image.alloc_height = alloc_height; + + const int ofs = + term->sixel.pos.row * term->sixel.image.width + term->sixel.pos.col; + + term->sixel.image.data = new_data; + term->sixel.image.p = &term->sixel.image.data[ofs]; + + return true; +} + +static bool +resize(struct terminal *term, int new_width_mutable, int new_height_mutable) +{ + LOG_DBG("resizing image: %dx%d -> %dx%d", + term->sixel.image.width, term->sixel.image.height, + new_width_mutable, new_height_mutable); + + if (unlikely(new_width_mutable > term->sixel.max_width)) { + LOG_WARN("maximum image width exceeded, truncating"); + new_width_mutable = term->sixel.max_width; + } + + if (unlikely(new_height_mutable > term->sixel.max_height)) { + LOG_WARN("maximum image height exceeded, truncating"); + new_height_mutable = term->sixel.max_height; + } + + if (unlikely(new_height_mutable == 0)) { + new_height_mutable = 6 * term->sixel.pan; + } + + uint32_t *old_data = term->sixel.image.data; + const int old_width = term->sixel.image.width; + const int old_height = term->sixel.image.height; + const int new_width = new_width_mutable; + const int new_height = new_height_mutable; + + if (unlikely(old_width == new_width && old_height == new_height)) + return true; + + const int sixel_row_height = 6 * term->sixel.pan; + const int alloc_new_height = + (new_height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; + + xassert(alloc_new_height >= new_height); + xassert(alloc_new_height - new_height < sixel_row_height); + + uint32_t *new_data = NULL; + const uint32_t bg = term->sixel.transparent_bg ? 0 : term->sixel.palette[0]; + + /* + * If the image is resized horizontally, or if it's opaque, we + * need to explicitly initialize the "new" pixels. + * + * When the image is *not* resized horizontally, we simply do a + * realloc(). In this case, there's no need to manually copy the + * old pixels. We do however need to initialize the new pixels + * since realloc() returns uninitialized memory. + * + * When the image *is* resized horizontally, we need to allocate + * new memory (when the width changes, the stride changes, and + * thus we cannot simply realloc()) + * + * If the default background is transparent, the new pixels need + * to be initialized to 0x0. We do this by using calloc(). + * + * If the default background is opaque, then we need to manually + * initialize the new pixels. + */ + const bool initialize_bg = + !term->sixel.transparent_bg || new_width == old_width; + + if (new_width == old_width) { + /* Width (and thus stride) is the same, so we can simply + * re-alloc the existing buffer */ + + new_data = realloc(old_data, new_width * alloc_new_height * sizeof(uint32_t)); + if (new_data == NULL) { + LOG_ERRNO("failed to reallocate sixel image buffer"); + return false; + } + + xassert(new_height > old_height); + + } else { + /* Width (and thus stride) change - need to allocate a new buffer */ + xassert(new_width > old_width); + const size_t pixels = new_width * alloc_new_height; + + new_data = !initialize_bg + ? xcalloc(pixels, sizeof(uint32_t)) + : xmalloc(pixels * sizeof(uint32_t)); + + /* Copy old rows, and initialize new columns to background color */ + const int row_copy_count = min(old_height, alloc_new_height); + const uint32_t *end = &new_data[row_copy_count * new_width]; + + for (uint32_t *n = new_data, *o = old_data; + n < end; + n += new_width, o += old_width) + { + memcpy(n, o, old_width * sizeof(uint32_t)); + memset_u32(&n[old_width], bg, new_width - old_width); + } + free(old_data); + } + + if (initialize_bg) { + memset_u32(&new_data[old_height * new_width], + bg, + (alloc_new_height - old_height) * new_width); + } + + xassert(new_data != NULL); + term->sixel.image.data = new_data; + term->sixel.image.width = new_width; + term->sixel.image.height = new_height; + term->sixel.image.alloc_height = alloc_new_height; + term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * new_width + term->sixel.pos.col]; + + return true; +} + +static void +sixel_add_generic(struct terminal *term, uint32_t *data, int stride, uint32_t color, + uint8_t sixel) +{ + const int pan = term->sixel.pan; + + for (int i = 0; i < 6; i++, sixel >>= 1) { + if (sixel & 1) { + for (int r = 0; r < pan; r++, data += stride) + *data = color; + } else + data += stride * pan; + } + + xassert(sixel == 0); +} + +static void ALWAYS_INLINE inline +sixel_add_ar_11(struct terminal *term, uint32_t *data, int stride, uint32_t color, + uint8_t sixel) +{ + xassert(term->sixel.pan == 1); + + if (sixel & 0x01) + *data = color; + data += stride; + if (sixel & 0x02) + *data = color; + data += stride; + if (sixel & 0x04) + *data = color; + data += stride; + if (sixel & 0x08) + *data = color; + data += stride; + if (sixel & 0x10) + *data = color; + data += stride; + if (sixel & 0x20) + *data = color; +} + +static void +sixel_add_many_generic(struct terminal *term, uint8_t c, unsigned count) +{ + int col = term->sixel.pos.col; + int width = term->sixel.image.width; + + count *= term->sixel.pad; + + if (unlikely(col + count - 1 >= width)) { + resize_horizontally(term, col + count); + width = term->sixel.image.width; + count = min(count, max(width - col, 0)); + + if (unlikely(count == 0)) + return; + } + + uint32_t color = term->sixel.color; + uint32_t *data = term->sixel.image.p; + uint32_t *end = data + count; + + term->sixel.pos.col = col + count; + term->sixel.image.p = end; + term->sixel.image.bottom_pixel |= c; + + for (; data < end; data++) + sixel_add_generic(term, data, width, color, c); + +} + +static void ALWAYS_INLINE inline +sixel_add_one_ar_11(struct terminal *term, uint8_t c) +{ + xassert(term->sixel.pan == 1); + xassert(term->sixel.pad == 1); + + int col = term->sixel.pos.col; + int width = term->sixel.image.width; + + if (unlikely(col >= width)) { + resize_horizontally(term, col + count); + width = term->sixel.image.width; + count = min(count, max(width - col, 0)); + + if (unlikely(count == 0)) + return; + } + + uint32_t *data = term->sixel.image.p; + + term->sixel.pos.col += 1; + term->sixel.image.p += 1; + term->sixel.image.bottom_pixel |= c; + + sixel_add_ar_11(term, data, width, term->sixel.color, c); +} + +static void +sixel_add_many_ar_11(struct terminal *term, uint8_t c, unsigned count) +{ + xassert(term->sixel.pan == 1); + xassert(term->sixel.pad == 1); + + int col = term->sixel.pos.col; + int width = term->sixel.image.width; + + if (unlikely(col + count - 1 >= width)) { + resize_horizontally(term, col + count); + width = term->sixel.image.width; + count = min(count, max(width - col, 0)); + + if (unlikely(count == 0)) + return; + } + + uint32_t color = term->sixel.color; + uint32_t *data = term->sixel.image.p; + uint32_t *end = data + count; + + term->sixel.pos.col += count; + term->sixel.image.p = end; + term->sixel.image.bottom_pixel |= c; + + for (; data < end; data++) + sixel_add_ar_11(term, data, width, color, c); + +} + +IGNORE_WARNING("-Wpedantic") + +static void +decsixel_generic(struct terminal *term, uint8_t c) +{ + switch (c) { + case '"': + term->sixel.state = SIXEL_DECGRA; + term->sixel.param = 0; + term->sixel.param_idx = 0; + break; + + case '!': + term->sixel.state = SIXEL_DECGRI; + term->sixel.param = 0; + term->sixel.param_idx = 0; + term->sixel.repeat_count = 1; + break; + + case '#': + term->sixel.state = SIXEL_DECGCI; + term->sixel.color_idx = 0; + term->sixel.param = 0; + term->sixel.param_idx = 0; + break; + + case '$': + if (likely(term->sixel.pos.col <= term->sixel.max_width)) { + /* + * We set, and keep, 'col' outside the image boundary when + * we've reached the maximum image height, to avoid also + * having to check the row vs image height in the common + * path in sixel_add(). + */ + term->sixel.pos.col = 0; + term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * term->sixel.image.width]; + } + break; + + case '-': /* GNL - Graphical New Line */ + term->sixel.pos.row += 6 * term->sixel.pan; + term->sixel.pos.col = 0; + term->sixel.image.bottom_pixel = 0; + term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * term->sixel.image.width]; + + if (term->sixel.pos.row >= term->sixel.image.alloc_height) { + if (!resize_vertically(term, term->sixel.pos.row + 6 * term->sixel.pan)) + term->sixel.pos.col = term->sixel.max_width + 1 * term->sixel.pad; + } + break; + + case '?' ... '~': + sixel_add_many_generic(term, c - 63, 1); + break; + + case ' ': + case '\n': + case '\r': + break; + + default: + LOG_WARN("invalid sixel character: '%c' at idx=%zu", c, count); + break; + } +} + +UNIGNORE_WARNINGS + +static void +decsixel_ar_11(struct terminal *term, uint8_t c) +{ + if (likely(c >= '?' && c <= '~')) + sixel_add_one_ar_11(term, c - 63); + else + decsixel_generic(term, c); +} + +static void +decgra(struct terminal *term, uint8_t c) +{ + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + term->sixel.param *= 10; + term->sixel.param += c - '0'; + break; + + case ';': + if (term->sixel.param_idx < ALEN(term->sixel.params)) + term->sixel.params[term->sixel.param_idx++] = term->sixel.param; + term->sixel.param = 0; + break; + + default: { + if (term->sixel.param_idx < ALEN(term->sixel.params)) + term->sixel.params[term->sixel.param_idx++] = term->sixel.param; + + int nparams = term->sixel.param_idx; + unsigned pan = nparams > 0 ? term->sixel.params[0] : 0; + unsigned pad = nparams > 1 ? term->sixel.params[1] : 0; + unsigned ph = nparams > 2 ? term->sixel.params[2] : 0; + unsigned pv = nparams > 3 ? term->sixel.params[3] : 0; + + pan = pan > 0 ? pan : 1; + pad = pad > 0 ? pad : 1; + + if (likely(term->sixel.image.width == 0 && + term->sixel.image.height == 0)) + { + term->sixel.pan = pan; + term->sixel.pad = pad; + } else { + /* + * Unsure what the VT340 does... + * + * We currently do *not* handle changing pan/pad in the + * middle of a sixel, since that means resizing/stretching + * the existing image. + * + * I'm *guessing* the VT340 simply changes the aspect + * ratio of all subsequent sixels. But, given the design + * of our implementation (the entire sixel is written to a + * single pixman image), we can't easily do that. + */ + LOG_WARN("sixel: unsupported: pan/pad changed after printing sixels"); + pan = term->sixel.pan; + pad = term->sixel.pad; + } + + pv *= pan; + ph *= pad; + + LOG_DBG("pan=%u, pad=%u (aspect ratio = %d:%d), size=%ux%u", + pan, pad, pan, pad, ph, pv); + + /* + * RA really only acts as a rectangular erase - it fills the + * specified area with the sixel background color[^1]. Nothing + * else. It does *not* affect cursor positioning. + * + * This means that if the emitted sixel is *smaller* than the + * RA, the text cursor will be placed "inside" the RA area. + * + * This means it would be more correct to view the RA area as + * a *separate* sixel image, that is then overlaid with the + * actual sixel. + * + * Still, RA _is_ a hint - the final image is _likely_ going + * to be this large. And, treating RA as a separate image + * prevents us from pre-allocating the final sixel image. + * + * So we don't. We use the RA as a hint, and pre-allocates the + * backing image buffer. + * + * [^1]: i.e. it's a NOP if the sixel is transparent + */ + if (ph >= term->sixel.image.height && pv >= term->sixel.image.width && + ph <= term->sixel.max_height && pv <= term->sixel.max_width) + { + /* + * TODO: always resize to a multiple of 6*pan? + * + * We're effectively doing that already, except + * sixel.image.height is set to ph, instead of the + * allocated height (which is always a multiple of 6*pan). + * + * If the user wants to emit a sixel that isn't a multiple + * of 6 pixels, the bottom sixel rows should all be empty, + * and (assuming a transparent sixel), trimmed when the + * final image is generated. + */ + resize(term, ph, pv); + } + + term->sixel.state = SIXEL_DECSIXEL; + + /* Update DCS put handler, since pan/pad may have changed */ + term->vt.dcs.put_handler = pan == 1 && pad == 1 + ? &sixel_put_ar_11 + : &sixel_put_generic; + + if (likely(pan == 1 && pad == 1)) + decsixel_ar_11(term, c); + else + decsixel_generic(term, c); + + break; + } + } +} + +IGNORE_WARNING("-Wpedantic") + +static void +decgri_generic(struct terminal *term, uint8_t c) +{ + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': { + unsigned param = term->sixel.param; + param *= 10; + param += c - '0'; + term->sixel.repeat_count = term->sixel.param = param; + break; + } + + case '?' ... '~': { + unsigned count = term->sixel.repeat_count; + if (unlikely(count == 0)) { + count = 1; + } + + sixel_add_many_generic(term, c - 63, count); + term->sixel.state = SIXEL_DECSIXEL; + break; + } + + default: + term->sixel.state = SIXEL_DECSIXEL; + term->vt.dcs.put_handler(term, c); + break; + } +} + +UNIGNORE_WARNINGS + +static void +decgri_ar_11(struct terminal *term, uint8_t c) +{ + if (likely(c >= '?' && c <= '~')) { + unsigned count = term->sixel.repeat_count; + if (unlikely(count == 0)) { + count = 1; + } + + sixel_add_many_ar_11(term, c - 63, count); + term->sixel.state = SIXEL_DECSIXEL; + } else + decgri_generic(term, c); +} + +static void +decgci(struct terminal *term, uint8_t c) +{ + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + term->sixel.param *= 10; + term->sixel.param += c - '0'; + break; + + case ';': + if (term->sixel.param_idx < ALEN(term->sixel.params)) + term->sixel.params[term->sixel.param_idx++] = term->sixel.param; + term->sixel.param = 0; + break; + + default: { + if (term->sixel.param_idx < ALEN(term->sixel.params)) + term->sixel.params[term->sixel.param_idx++] = term->sixel.param; + + int nparams = term->sixel.param_idx; + + if (nparams > 0) + term->sixel.color_idx = min(term->sixel.params[0], term->sixel.palette_size - 1); + + if (nparams > 4) { + unsigned format = term->sixel.params[1]; + int c1 = term->sixel.params[2]; + int c2 = term->sixel.params[3]; + int c3 = term->sixel.params[4]; + + switch (format) { + case 1: { /* HLS */ + int hue = min(c1, 360); + int lum = min(c2, 100); + int sat = min(c3, 100); + + /* + * Sixel's HLS use the following primary color hues: + * blue: 0° + * red: 120° + * green: 240° + * + * While "standard" HSL uses: + * red: 0° + * green: 120° + * blue: 240° + */ + hue = (hue + 240) % 360; + + uint32_t rgb = hsl_to_rgb(hue, sat, lum); + + LOG_DBG("setting palette #%d = HLS %hhu/%hhu/%hhu (0x%06x)", + term->sixel.color_idx, hue, lum, sat, rgb); + + term->sixel.palette[term->sixel.color_idx] = 0xffu << 24 | rgb; + break; + } + + case 2: { /* RGB */ + uint16_t r = 255 * min(c1, 100) / 100; + uint16_t g = 255 * min(c2, 100) / 100; + uint16_t b = 255 * min(c3, 100) / 100; + + LOG_DBG("setting palette #%d = RGB %hu/%hu/%hu", + term->sixel.color_idx, r, g, b); + + term->sixel.palette[term->sixel.color_idx] = color_decode_srgb(term, r, g, b); + break; + } + } + } else + term->sixel.color = term->sixel.palette[term->sixel.color_idx]; + + term->sixel.state = SIXEL_DECSIXEL; + + if (likely(term->sixel.pan == 1 && term->sixel.pad == 1)) + decsixel_ar_11(term, c); + else + decsixel_generic(term, c); + break; + } + } +} + +static void +sixel_put_generic(struct terminal *term, uint8_t c) +{ + switch (term->sixel.state) { + case SIXEL_DECSIXEL: decsixel_generic(term, c); break; + case SIXEL_DECGRA: decgra(term, c); break; + case SIXEL_DECGRI: decgri_generic(term, c); break; + case SIXEL_DECGCI: decgci(term, c); break; + } + + count++; +} + +static void +sixel_put_ar_11(struct terminal *term, uint8_t c) +{ + switch (term->sixel.state) { + case SIXEL_DECSIXEL: decsixel_ar_11(term, c); break; + case SIXEL_DECGRA: decgra(term, c); break; + case SIXEL_DECGRI: decgri_ar_11(term, c); break; + case SIXEL_DECGCI: decgci(term, c); break; + } + + count++; +} + +void +sixel_colors_report_current(struct terminal *term) +{ + char reply[24]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[?1;0;%uS", term->sixel.palette_size); + term_to_slave(term, reply, n); + LOG_DBG("query response for current color count: %u", term->sixel.palette_size); +} + +void +sixel_colors_reset(struct terminal *term) +{ + LOG_DBG("sixel palette size reset to %u", SIXEL_MAX_COLORS); + + free(term->sixel.palette); + term->sixel.palette = NULL; + + term->sixel.palette_size = SIXEL_MAX_COLORS; + sixel_colors_report_current(term); +} + +void +sixel_colors_set(struct terminal *term, unsigned count) +{ + unsigned new_palette_size = min(max(2, count), SIXEL_MAX_COLORS); + LOG_DBG("sixel palette size set to %u", new_palette_size); + + free(term->sixel.private_palette); + free(term->sixel.shared_palette); + term->sixel.private_palette = NULL; + term->sixel.shared_palette = NULL; + + term->sixel.palette_size = new_palette_size; + sixel_colors_report_current(term); +} + +void +sixel_colors_report_max(struct terminal *term) +{ + char reply[24]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[?1;0;%uS", SIXEL_MAX_COLORS); + term_to_slave(term, reply, n); + LOG_DBG("query response for max color count: %u", SIXEL_MAX_COLORS); +} + +void +sixel_geometry_report_current(struct terminal *term) +{ + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[?2;0;%u;%uS", + min(term->cols * term->cell_width, term->sixel.max_width), + min(term->rows * term->cell_height, term->sixel.max_height)); + term_to_slave(term, reply, n); + + LOG_DBG("query response for current sixel geometry: %ux%u", + term->sixel.max_width, term->sixel.max_height); +} + +void +sixel_geometry_reset(struct terminal *term) +{ + LOG_DBG("sixel geometry reset to %ux%u", SIXEL_MAX_WIDTH, SIXEL_MAX_HEIGHT); + term->sixel.max_width = SIXEL_MAX_WIDTH; + term->sixel.max_height = SIXEL_MAX_HEIGHT; + sixel_geometry_report_current(term); +} + +void +sixel_geometry_set(struct terminal *term, unsigned width, unsigned height) +{ + LOG_DBG("sixel geometry set to %ux%u", width, height); + term->sixel.max_width = width; + term->sixel.max_height = height; + sixel_geometry_report_current(term); +} + +void +sixel_geometry_report_max(struct terminal *term) +{ + unsigned max_width = term->sixel.max_width; + unsigned max_height = term->sixel.max_height; + + char reply[64]; + size_t n = xsnprintf(reply, sizeof(reply), "\033[?2;0;%u;%uS", max_width, max_height); + term_to_slave(term, reply, n); + + LOG_DBG("query response for max sixel geometry: %ux%u", + max_width, max_height); +} diff --git a/sixel.h b/sixel.h new file mode 100644 index 0000000..ab8a505 --- /dev/null +++ b/sixel.h @@ -0,0 +1,50 @@ +#pragma once + +#include "terminal.h" + +#define SIXEL_MAX_COLORS 1024u +#define SIXEL_MAX_WIDTH 10000u +#define SIXEL_MAX_HEIGHT 10000u + +typedef void (*sixel_put)(struct terminal *term, uint8_t c); + +void sixel_fini(struct terminal *term); + +sixel_put sixel_init(struct terminal *term, int p1, int p2, int p3); +void sixel_unhook(struct terminal *term); + +void sixel_destroy(struct sixel *sixel); +void sixel_destroy_all(struct terminal *term); + +void sixel_scroll_up(struct terminal *term, int rows); +void sixel_scroll_down(struct terminal *term, int rows); + +void sixel_cell_size_changed(struct terminal *term); +void sixel_sync_cache(const struct terminal *term, struct sixel *sixel); + +void sixel_reflow_grid(struct terminal *term, struct grid *grid); + +/* Shortcut for sixel_reflow_grid(normal) + sixel_reflow_grid(alt) */ +void sixel_reflow(struct terminal *term); + +/* + * Remove sixel data from the specified location. Used when printing + * or erasing characters, and when emitting new sixel images, to + * remove sixel data that would otherwise be rendered on-top. + * + * Row numbers are relative to the current grid offset + */ +void sixel_overwrite_by_rectangle( + struct terminal *term, int row, int col, int height, int width); +void sixel_overwrite_by_row(struct terminal *term, int row, int col, int width); +void sixel_overwrite_at_cursor(struct terminal *term, int width); + +void sixel_colors_report_current(struct terminal *term); +void sixel_colors_reset(struct terminal *term); +void sixel_colors_set(struct terminal *term, unsigned count); +void sixel_colors_report_max(struct terminal *term); + +void sixel_geometry_report_current(struct terminal *term); +void sixel_geometry_reset(struct terminal *term); +void sixel_geometry_set(struct terminal *term, unsigned width, unsigned height); +void sixel_geometry_report_max(struct terminal *term); diff --git a/slave.c b/slave.c new file mode 100644 index 0000000..6289937 --- /dev/null +++ b/slave.c @@ -0,0 +1,573 @@ +#include "slave.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define LOG_MODULE "slave" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "debug.h" +#include "macros.h" +#include "tokenize.h" +#include "util.h" +#include "xmalloc.h" + +extern char **environ; + +struct environ { + size_t count; + char **envp; +}; + +#if !defined(EXECVPE) +static char * +find_file_in_path(const char *file) +{ + if (strchr(file, '/') != NULL) + return xstrdup(file); + + const char *env_path = getenv("PATH"); + char *path_list = NULL; + + if (env_path != NULL && env_path[0] != '\0') + path_list = xstrdup(env_path); + else { + size_t sc_path_len = confstr(_CS_PATH, NULL, 0); + if (sc_path_len > 0) { + path_list = xmalloc(sc_path_len); + confstr(_CS_PATH, path_list, sc_path_len); + } else + return xstrdup(file); + } + + for (const char *path = strtok(path_list, ":"); + path != NULL; + path = strtok(NULL, ":")) + { + char *full = xstrjoin3(path, "/", file); + if (access(full, F_OK) == 0) { + free(path_list); + return full; + } + + free(full); + } + + free(path_list); + return xstrdup(file); +} + +static int +foot_execvpe(const char *file, char *const argv[], char *const envp[]) +{ + char *path = find_file_in_path(file); + int ret = execve(path, argv, envp); + + /* + * Getting here is an error + */ + free(path); + return ret; +} + +#else /* EXECVPE */ + +#define foot_execvpe(file, argv, envp) execvpe(file, argv, envp) + +#endif /* EXECVPE */ + +static bool +is_valid_shell(const char *shell) +{ + FILE *f = fopen("/etc/shells", "r"); + if (f == NULL) + goto err; + + char *_line = NULL; + size_t count = 0; + + while (true) { + errno = 0; + ssize_t ret = getline(&_line, &count, f); + + if (ret < 0) { + free(_line); + break; + } + + char *line = _line; + { + while (isspace(*line)) + line++; + if (line[0] != '\0') { + char *end = line + strlen(line) - 1; + while (isspace(*end)) + end--; + *(end + 1) = '\0'; + } + } + + if (line[0] == '#') + continue; + + if (streq(line, shell)) { + fclose(f); + return true; + } + } + +err: + if (f != NULL) + fclose(f); + return false; +} + +enum user_notification_ret_t {UN_OK, UN_NO_MORE, UN_FAIL}; + +static enum user_notification_ret_t +emit_one_notification(int fd, const struct user_notification *notif) +{ + const char *prefix = NULL; + const char *postfix = "\033[m\n"; + + switch (notif->kind) { + case USER_NOTIFICATION_DEPRECATED: + prefix = "\033[33;1mdeprecated\033[39;22m: "; + break; + + case USER_NOTIFICATION_WARNING: + prefix = "\033[33;1mwarning\033[39;22m: "; + break; + + case USER_NOTIFICATION_ERROR: + prefix = "\033[31;1merror\033[39;22m: "; + break; + } + + xassert(prefix != NULL); + + if (write(fd, prefix, strlen(prefix)) < 0 || + write(fd, "foot: ", 6) < 0 || + write(fd, notif->text, strlen(notif->text)) < 0 || + write(fd, postfix, strlen(postfix)) < 0) + { + /* + * The main process is blocking and waiting for us to close + * the error pipe. Thus, pts data will *not* be processed + * until we've exec:d. This means we cannot write anymore once + * the kernel buffer is full. Don't treat this as a fatal + * error. + */ + if (errno == EWOULDBLOCK || errno == EAGAIN) + return UN_NO_MORE; + else { + LOG_ERRNO("failed to write user-notification"); + return UN_FAIL; + } + } + + return UN_OK; +} + +static bool +emit_notifications_of_kind(int fd, + const user_notifications_t *notifications, + enum user_notification_kind kind) +{ + tll_foreach(*notifications, it) { + if (it->item.kind == kind) { + switch (emit_one_notification(fd, &it->item)) { + case UN_OK: + break; + case UN_NO_MORE: + return true; + case UN_FAIL: + return false; + } + } + } + + return true; +} + +static bool +emit_notifications(int fd, const user_notifications_t *notifications) +{ + return + emit_notifications_of_kind(fd, notifications, USER_NOTIFICATION_ERROR) && + emit_notifications_of_kind(fd, notifications, USER_NOTIFICATION_WARNING) && + emit_notifications_of_kind(fd, notifications, USER_NOTIFICATION_DEPRECATED); +} + +static noreturn void +slave_exec(int ptmx, char *argv[], char *const envp[], int err_fd, + bool login_shell, const user_notifications_t *notifications) +{ + int pts = -1; + const char *pts_name = ptsname(ptmx); + + if (grantpt(ptmx) == -1) { + LOG_ERRNO("failed to grantpt()"); + goto err; + } + if (unlockpt(ptmx) == -1) { + LOG_ERRNO("failed to unlockpt()"); + goto err; + } + + close(ptmx); + ptmx = -1; + + if (setsid() == -1) { + LOG_ERRNO("failed to setsid()"); + goto err; + } + + pts = open(pts_name, O_RDWR); + if (pts == -1) { + LOG_ERRNO("failed to open pseudo terminal slave device"); + goto err; + } + + if (ioctl(pts, TIOCSCTTY, 0) < 0) { + LOG_ERRNO("failed to configure controlling terminal"); + goto err; + } + +#ifdef IUTF8 + { + struct termios flags; + if (tcgetattr(pts, &flags) < 0) { + LOG_ERRNO("failed to get terminal attributes"); + goto err; + } + + flags.c_iflag |= IUTF8; + if (tcsetattr(pts, TCSANOW, &flags) < 0) { + LOG_ERRNO("failed to set IUTF8 terminal attribute"); + goto err; + } + } +#endif + + if (tll_length(*notifications) > 0) { + int flags = fcntl(pts, F_GETFL); + if (flags < 0) + goto err; + if (fcntl(pts, F_SETFL, flags | O_NONBLOCK) < 0) + goto err; + + if (!emit_notifications(pts, notifications)) + goto err; + + fcntl(pts, F_SETFL, flags); + } + + if (dup2(pts, STDIN_FILENO) == -1 || + dup2(pts, STDOUT_FILENO) == -1 || + dup2(pts, STDERR_FILENO) == -1) + { + LOG_ERRNO("failed to dup stdin/stdout/stderr"); + goto err; + } + + close(pts); + pts = -1; + + const char *file; + if (login_shell) { + file = xstrdup(argv[0]); + + char *arg0 = xmalloc(strlen(argv[0]) + 1 + 1); + arg0[0] = '-'; + arg0[1] = '\0'; + strcat(arg0, argv[0]); + + argv[0] = arg0; + } else + file = argv[0]; + + foot_execvpe(file, argv, envp); + +err: + (void)!write(err_fd, &errno, sizeof(errno)); + if (pts != -1) + close(pts); + if (ptmx != -1) + close(ptmx); + close(err_fd); + _exit(errno); +} + +static bool +env_matches_var_name(const char *e, const char *name) +{ + const size_t e_len = strlen(e); + const size_t name_len = strlen(name); + + if (e_len <= name_len) + return false; + if (memcmp(e, name, name_len) != 0) + return false; + if (e[name_len] != '=') + return false; + return true; +} + +static void +add_to_env(struct environ *env, const char *name, const char *value) +{ + if (env->envp == NULL) + setenv(name, value, 1); + else { + char *e = xstrjoin3(name, "=", value); + + /* Search for existing variable. If found, replace it with the + new value */ + for (size_t i = 0; i < env->count; i++) { + if (env_matches_var_name(env->envp[i], name)) { + free(env->envp[i]); + env->envp[i] = e; + return; + } + } + + /* If the variable does not already exist, add it */ + env->envp = xrealloc(env->envp, (env->count + 2) * sizeof(env->envp[0])); + env->envp[env->count++] = e; + env->envp[env->count] = NULL; + } +} + +static void +del_from_env(struct environ *env, const char *name) +{ + if (env->envp == NULL) + unsetenv(name); + else { + for (size_t i = 0; i < env->count; i++) { + if (env_matches_var_name(env->envp[i], name)) { + free(env->envp[i]); + memmove(&env->envp[i], + &env->envp[i + 1], + (env->count - i) * sizeof(env->envp[0])); + env->count--; + xassert(env->envp[env->count] == NULL); + break; + } + } + } +} + +pid_t +slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, + const char *const *envp, const env_var_list_t *extra_env_vars, + const char *term_env, const char *conf_shell, bool login_shell, + const user_notifications_t *notifications) +{ + int fork_pipe[2]; + if (pipe2(fork_pipe, O_CLOEXEC) < 0) { + LOG_ERRNO("failed to create pipe"); + return -1; + } + + pid_t pid = fork(); + switch (pid) { + case -1: + LOG_ERRNO("failed to fork"); + close(fork_pipe[0]); + close(fork_pipe[1]); + return -1; + + case 0: + /* Child */ + close(fork_pipe[0]); /* Close read end */ + + if (chdir(cwd) < 0) { + const int errno_copy = errno; + LOG_ERRNO("failed to change working directory to %s", cwd); + (void)!write(fork_pipe[1], &errno_copy, sizeof(errno_copy)); + _exit(errno_copy); + } + + /* Restore signal mask, and SIG_IGN'd signals */ + struct sigaction dfl = {.sa_handler = SIG_DFL}; + sigemptyset(&dfl.sa_mask); + sigset_t mask; + sigemptyset(&mask); + + if (sigprocmask(SIG_SETMASK, &mask, NULL) < 0 || + sigaction(SIGHUP, &dfl, NULL) < 0 || + sigaction(SIGPIPE, &dfl, NULL) < 0) + { + const int errno_copy = errno; + LOG_ERRNO_P(errno, "failed to restore signals"); + (void)!write(fork_pipe[1], &errno_copy, sizeof(errno_copy)); + _exit(errno_copy); + } + + /* Create a mutable copy of the environment */ + struct environ custom_env = {0}; + if (envp != NULL) { + for (const char *const *e = envp; *e != NULL; e++) + custom_env.count++; + + custom_env.envp = xcalloc( + custom_env.count + 1, sizeof(custom_env.envp[0])); + + size_t i = 0; + for (const char *const *e = envp; *e != NULL; e++, i++) + custom_env.envp[i] = xstrdup(*e); + xassert(custom_env.envp[custom_env.count] == NULL); + } + + add_to_env(&custom_env, "TERM", term_env); + add_to_env(&custom_env, "COLORTERM", "truecolor"); + add_to_env(&custom_env, "PWD", cwd); + + del_from_env(&custom_env, "TERM_PROGRAM"); /* Wezterm, Ghostty */ + del_from_env(&custom_env, "TERM_PROGRAM_VERSION"); /* Wezterm, Ghostty */ + del_from_env(&custom_env, "TERMINAL_NAME"); /* Contour */ + del_from_env(&custom_env, "TERMINAL_VERSION_STRING"); /* Contour */ + del_from_env(&custom_env, "TERMINAL_VERSION_TRIPLE"); /* Contour */ + + /* XTerm specific */ + del_from_env(&custom_env, "XTERM_SHELL"); + del_from_env(&custom_env, "XTERM_VERSION"); + del_from_env(&custom_env, "XTERM_LOCALE"); + + /* Mlterm specific */ + del_from_env(&custom_env, "MLTERM"); + + /* Zutty specific */ + del_from_env(&custom_env, "ZUTTY_VERSION"); + + /* Ghostty specific */ + del_from_env(&custom_env, "GHOSTTY_BIN_DIR"); + del_from_env(&custom_env, "GHOSTTY_SHELL_INTEGRATION_NO_SUDO"); + del_from_env(&custom_env, "GHOSTTY_RESOURCES_DIR"); + + /* Kitty specific */ + del_from_env(&custom_env, "KITTY_WINDOW_ID"); + del_from_env(&custom_env, "KITTY_PID"); + del_from_env(&custom_env, "KITTY_PUBLIC_KEY"); + del_from_env(&custom_env, "KITTY_INSTALLATION_DIR"); + + /* Contour specific */ + del_from_env(&custom_env, "CONTOUR_PROFILE"); + + /* Wezterm specific */ + del_from_env(&custom_env, "WEZTERM_PANE"); + del_from_env(&custom_env, "WEZTERM_EXECUTABLE"); + del_from_env(&custom_env, "WEZTERM_CONFIG_FILE"); + del_from_env(&custom_env, "WEZTERM_EXECUTABLE_DIR"); + del_from_env(&custom_env, "WEZTERM_UNIX_SOCKET"); + del_from_env(&custom_env, "WEZTERM_CONFIG_DIR"); + + /* Alacritty specific */ + del_from_env(&custom_env, "ALACRITTY_LOG"); + del_from_env(&custom_env, "ALACRITTY_WINDOW_ID"); + del_from_env(&custom_env, "ALACRITTY_SOCKET"); + + /* VTE, gnome-terminal, kgx etc */ + del_from_env(&custom_env, "VTE_VERSION"); + del_from_env(&custom_env, "GNOME_TERMINAL_SERVICE"); + del_from_env(&custom_env, "GNOME_TERMINAL_SCREEN"); + +#if defined(FOOT_TERMINFO_PATH) + add_to_env(&custom_env, "TERMINFO", FOOT_TERMINFO_PATH); +#endif + + if (extra_env_vars != NULL) { + tll_foreach(*extra_env_vars, it) { + const char *name = it->item.name; + const char *value = it->item.value; + + if (strlen(value) == 0) + del_from_env(&custom_env, name); + else + add_to_env(&custom_env, name, value); + } + } + + char **_shell_argv = NULL; + char **shell_argv = NULL; + + if (argc == 0) { + if (!tokenize_cmdline(conf_shell, &_shell_argv)) { + (void)!write(fork_pipe[1], &errno, sizeof(errno)); + _exit(0); + } + shell_argv = _shell_argv; + } else { + size_t count = 0; + for (; argv[count] != NULL; count++) + ; + shell_argv = xmalloc((count + 1) * sizeof(shell_argv[0])); + for (size_t i = 0; i < count; i++) + shell_argv[i] = argv[i]; + shell_argv[count] = NULL; + } + + if (is_valid_shell(shell_argv[0])) + add_to_env(&custom_env, "SHELL", shell_argv[0]); + + slave_exec(ptmx, shell_argv, + custom_env.envp != NULL ? custom_env.envp : environ, + fork_pipe[1], login_shell, notifications); + BUG("Unexpected return from slave_exec()"); + break; + + default: { + + /* + * Don't stay in CWD, since it may be an ephemeral path. For + * example, it may be a mount point of, say, a thumb drive. Us + * keeping it open will prevent the user from unmounting it. + */ + (void)!!chdir("/"); + + close(fork_pipe[1]); /* Close write end */ + LOG_DBG("slave has PID %d", pid); + + int errno_copy; + static_assert(sizeof(errno) == sizeof(errno_copy), "errno size mismatch"); + + ssize_t ret = read(fork_pipe[0], &errno_copy, sizeof(errno_copy)); + close(fork_pipe[0]); + + if (ret < 0) { + LOG_ERRNO("failed to read from pipe"); + return -1; + } else if (ret == sizeof(errno_copy)) { + LOG_ERRNO_P( + errno_copy, "%s: failed to execute", + argc == 0 ? conf_shell : argv[0]); + return -1; + } else + LOG_DBG("%s: successfully started", conf_shell); + + int fd_flags; + if ((fd_flags = fcntl(ptmx, F_GETFD)) < 0 || + fcntl(ptmx, F_SETFD, fd_flags | FD_CLOEXEC) < 0) + { + LOG_ERRNO("failed to set FD_CLOEXEC on ptmx"); + return -1; + } + break; + } + } + + return pid; +} diff --git a/slave.h b/slave.h new file mode 100644 index 0000000..26d93ab --- /dev/null +++ b/slave.h @@ -0,0 +1,13 @@ +#pragma once +#include + +#include + +#include "config.h" +#include "user-notification.h" + +pid_t slave_spawn( + int ptmx, int argc, const char *cwd, char *const *argv, const char *const *envp, + const env_var_list_t *extra_env_vars, const char *term_env, + const char *conf_shell, bool login_shell, + const user_notifications_t *notifications); diff --git a/spawn.c b/spawn.c new file mode 100644 index 0000000..17c821b --- /dev/null +++ b/spawn.c @@ -0,0 +1,205 @@ +#include "spawn.h" + +#include +#include +#include +#include + +#include +#include +#include + +#define LOG_MODULE "spawn" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" +#include "xmalloc.h" + +pid_t +spawn(struct reaper *reaper, const char *cwd, char *const argv[], + int stdin_fd, int stdout_fd, int stderr_fd, reaper_cb cb, void *cb_data, + const char *xdg_activation_token) +{ + int pipe_fds[2] = {-1, -1}; + if (pipe2(pipe_fds, O_CLOEXEC) < 0) { + LOG_ERRNO("failed to create pipe"); + goto err; + } + + pid_t pid = fork(); + if (pid < 0) { + LOG_ERRNO("failed to fork"); + goto err; + } + + if (pid == 0) { + /* Child */ + close(pipe_fds[0]); + + if (setsid() < 0) + goto child_err; + + /* Clear signal mask */ + sigset_t mask; + sigemptyset(&mask); + if (sigprocmask(SIG_SETMASK, &mask, NULL) < 0) + goto child_err; + + /* Restore ignored (SIG_IGN) signals */ + struct sigaction dfl = {.sa_handler = SIG_DFL}; + sigemptyset(&dfl.sa_mask); + if (sigaction(SIGHUP, &dfl, NULL) < 0 || + sigaction(SIGPIPE, &dfl, NULL) < 0) + { + goto child_err; + } + + if (cwd != NULL) { + setenv("PWD", cwd, 1); + if (chdir(cwd) < 0) { + LOG_WARN("failed to change working directory to %s: %s", + cwd, strerror(errno)); + } + } + + if (xdg_activation_token != NULL) { + setenv("XDG_ACTIVATION_TOKEN", xdg_activation_token, 1); + + if (getenv("DISPLAY") != NULL) + setenv("DESKTOP_STARTUP_ID", xdg_activation_token, 1); + } + + bool close_stderr = stderr_fd >= 0; + bool close_stdout = stdout_fd >= 0 && stdout_fd != stderr_fd; + bool close_stdin = stdin_fd >= 0 && stdin_fd != stdout_fd && stdin_fd != stderr_fd; + + if ((stdin_fd >= 0 && (dup2(stdin_fd, STDIN_FILENO) < 0 + || (close_stdin && close(stdin_fd) < 0))) || + (stdout_fd >= 0 && (dup2(stdout_fd, STDOUT_FILENO) < 0 + || (close_stdout && close(stdout_fd) < 0))) || + (stderr_fd >= 0 && (dup2(stderr_fd, STDERR_FILENO) < 0 + || (close_stderr && close(stderr_fd) < 0))) || + execvp(argv[0], argv) < 0) + { + goto child_err; + } + + xassert(false); + _exit(errno); + + child_err: + ; + const int errno_copy = errno; + (void)!write(pipe_fds[1], &errno_copy, sizeof(errno_copy)); + _exit(errno_copy); + } + + /* Parent */ + close(pipe_fds[1]); + + int errno_copy; + static_assert(sizeof(errno_copy) == sizeof(errno), "errno size mismatch"); + + ssize_t ret = read(pipe_fds[0], &errno_copy, sizeof(errno_copy)); + close(pipe_fds[0]); + + if (ret == 0) { + reaper_add(reaper, pid, cb, cb_data); + return pid; + } else if (ret < 0) { + LOG_ERRNO("failed to read from pipe"); + return -1; + } else { + LOG_ERRNO_P(errno_copy, "%s: failed to spawn", argv[0]); + errno = errno_copy; + waitpid(pid, NULL, 0); + return -1; + } + +err: + if (pipe_fds[0] != -1) + close(pipe_fds[0]); + if (pipe_fds[1] != -1) + close(pipe_fds[1]); + return -1; +} + +bool +spawn_expand_template(const struct config_spawn_template *template, + size_t key_count, + const char *key_names[static key_count], + const char *key_values[static key_count], + size_t *argc, char ***argv) +{ + *argc = 0; + *argv = NULL; + + for (; template->argv.args[*argc] != NULL; (*argc)++) + ; + +#define append(s, n) \ + do { \ + expanded = xrealloc(expanded, len + (n) + 1); \ + memcpy(&expanded[len], s, n); \ + len += n; \ + expanded[len] = '\0'; \ + } while (0) + + *argv = xmalloc((*argc + 1) * sizeof((*argv)[0])); + + /* Expand the provided keys */ + for (size_t i = 0; i < *argc; i++) { + size_t len = 0; + char *expanded = NULL; + + char *start = NULL; + char *last_end = template->argv.args[i]; + + while ((start = strstr(last_end, "${")) != NULL) { + /* Append everything from the last template's end to this + * one's beginning */ + append(last_end, start - last_end); + + /* Find end of template */ + start += 2; + char *end = strstr(start, "}"); + + if (end == NULL) { + /* Ensure final append() copies the unclosed '${' */ + last_end = start - 2; + LOG_WARN("notify: unclosed template: %s", last_end); + break; + } + + /* Expand template */ + bool valid_key = false; + for (size_t j = 0; j < key_count; j++) { + if (strncmp(start, key_names[j], end - start) != 0) + continue; + + append(key_values[j], strlen(key_values[j])); + valid_key = true; + break; + } + + if (!valid_key) { + /* Unrecognized template - append it as-is */ + start -= 2; + append(start, end + 1 - start); + LOG_WARN("notify: unrecognized template: %.*s", + (int)(end + 1 - start), start); + } + + last_end = end + 1; + } + + append( + last_end, + template->argv.args[i] + strlen(template->argv.args[i]) - last_end); + (*argv)[i] = expanded; + } + (*argv)[*argc] = NULL; + +#undef append + return true; +} diff --git a/spawn.h b/spawn.h new file mode 100644 index 0000000..1693f1a --- /dev/null +++ b/spawn.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +#include "config.h" +#include "reaper.h" + +pid_t spawn(struct reaper *reaper, const char *cwd, char *const argv[], + int stdin_fd, int stdout_fd, int stderr_fd, + reaper_cb cb, void *cb_data, const char *xdg_activation_token); + +bool spawn_expand_template( + const struct config_spawn_template *template, + size_t key_count, const char *key_names[static key_count], + const char *key_values[static key_count], size_t *argc, char ***argv); diff --git a/stride.h b/stride.h new file mode 100644 index 0000000..b2c71a7 --- /dev/null +++ b/stride.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +static inline int +stride_for_format_and_width(pixman_format_code_t format, int width) +{ + return (((PIXMAN_FORMAT_BPP(format) * width + 7) / 8 + 4 - 1) & -4); +} diff --git a/subprojects/fcft.wrap b/subprojects/fcft.wrap new file mode 100644 index 0000000..d2709d4 --- /dev/null +++ b/subprojects/fcft.wrap @@ -0,0 +1,3 @@ +[wrap-git] +url = https://codeberg.org/dnkl/fcft.git +revision = master diff --git a/subprojects/tllist.wrap b/subprojects/tllist.wrap new file mode 100644 index 0000000..75f395a --- /dev/null +++ b/subprojects/tllist.wrap @@ -0,0 +1,3 @@ +[wrap-git] +url = https://codeberg.org/dnkl/tllist.git +revision = master diff --git a/subprojects/wayland-protocols.wrap b/subprojects/wayland-protocols.wrap new file mode 100644 index 0000000..74e5e91 --- /dev/null +++ b/subprojects/wayland-protocols.wrap @@ -0,0 +1,3 @@ +[wrap-git] +url = https://gitlab.freedesktop.org/wayland/wayland-protocols.git +revision = main diff --git a/terminal.c b/terminal.c new file mode 100644 index 0000000..0b9f56d --- /dev/null +++ b/terminal.c @@ -0,0 +1,5363 @@ +#include "terminal.h" + +#if defined(__GLIBC__) +#include +#endif +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define LOG_MODULE "terminal" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "async.h" +#include "commands.h" +#include "config.h" +#include "debug.h" +#include "emoji-variation-sequences.h" +#include "extract.h" +#include "grid.h" +#include "ime.h" +#include "input.h" +#include "notify.h" +#include "quirks.h" +#include "reaper.h" +#include "render.h" +#include "selection.h" +#include "shm.h" +#include "sixel.h" +#include "slave.h" +#include "spawn.h" +#include "url-mode.h" +#include "util.h" +#include "vt.h" +#include "xmalloc.h" +#include "xsnprintf.h" + +#define PTMX_TIMING 0 + +static void +enqueue_data_for_slave(const void *data, size_t len, size_t offset, + ptmx_buffer_list_t *buffer_list) +{ + struct ptmx_buffer queued = { + .data = xmemdup(data, len), + .len = len, + .idx = offset, + }; + tll_push_back(*buffer_list, queued); +} + +static bool +data_to_slave(struct terminal *term, const void *data, size_t len, + ptmx_buffer_list_t *buffer_list) +{ + /* + * Try a synchronous write first. If we fail to write everything, + * switch to asynchronous. + */ + + size_t async_idx = 0; + switch (async_write(term->ptmx, data, len, &async_idx)) { + case ASYNC_WRITE_REMAIN: + /* Switch to asynchronous mode; let FDM write the remaining data */ + if (!fdm_event_add(term->fdm, term->ptmx, EPOLLOUT)) + return false; + enqueue_data_for_slave(data, len, async_idx, buffer_list); + return true; + + case ASYNC_WRITE_DONE: + return true; + + case ASYNC_WRITE_ERR: + LOG_ERRNO("failed to synchronously write %zu bytes to slave", len); + return false; + } + + BUG("Unexpected async_write() return value"); + return false; +} + +bool +term_paste_data_to_slave(struct terminal *term, const void *data, size_t len) +{ + xassert(term->is_sending_paste_data); + + if (term->ptmx < 0) { + /* We're probably in "hold" */ + return false; + } + + if (tll_length(term->ptmx_paste_buffers) > 0) { + /* Don't even try to send data *now* if there's queued up + * data, since that would result in events arriving out of + * order. */ + enqueue_data_for_slave(data, len, 0, &term->ptmx_paste_buffers); + return true; + } + + return data_to_slave(term, data, len, &term->ptmx_paste_buffers); +} + +bool +term_to_slave(struct terminal *term, const void *data, size_t len) +{ + if (term->ptmx < 0) { + /* We're probably in "hold" */ + return false; + } + + if (unlikely(tll_length(term->ptmx_buffers) > 0 || + term->is_sending_paste_data || + tll_length(term->ptmx_paste_buffers) > 0)) + { + /* + * Don't even try to send data *now* if there's queued up + * data, since that would result in events arriving out of + * order. + * + * Furthermore, if we're currently sending paste data to the + * client, do *not* mix that stream with other events + * (https://codeberg.org/dnkl/foot/issues/101). + */ + enqueue_data_for_slave(data, len, 0, &term->ptmx_buffers); + return true; + } + + return data_to_slave(term, data, len, &term->ptmx_buffers); +} + +static bool +fdm_ptmx_out(struct fdm *fdm, int fd, int events, void *data) +{ + struct terminal *term = data; + + /* If there is no queued data, then we shouldn't be in asynchronous mode */ + xassert(tll_length(term->ptmx_buffers) > 0 || + tll_length(term->ptmx_paste_buffers) > 0); + + /* Writes a single buffer, returns if not all of it could be written */ +#define write_one_buffer(buffer_list) \ + { \ + switch (async_write(term->ptmx, it->item.data, it->item.len, &it->item.idx)) { \ + case ASYNC_WRITE_DONE: \ + free(it->item.data); \ + tll_remove(buffer_list, it); \ + break; \ + case ASYNC_WRITE_REMAIN: \ + /* to_slave() updated it->item.idx */ \ + return true; \ + case ASYNC_WRITE_ERR: \ + LOG_ERRNO("failed to asynchronously write %zu bytes to slave", \ + it->item.len - it->item.idx); \ + return false; \ + } \ + } + + tll_foreach(term->ptmx_paste_buffers, it) + write_one_buffer(term->ptmx_paste_buffers); + + /* If we get here, *all* paste data buffers were successfully + * flushed */ + + if (!term->is_sending_paste_data) { + tll_foreach(term->ptmx_buffers, it) + write_one_buffer(term->ptmx_buffers); + } + + /* + * If we get here, *all* buffers were successfully flushed. + * + * Or, we're still sending paste data, in which case we do *not* + * want to send the "normal" queued up data + * + * In both cases, we want to *disable* the FDM callback since + * otherwise we'd just be called right away again, with nothing to + * write. + */ + fdm_event_del(term->fdm, term->ptmx, EPOLLOUT); + return true; +} + +static bool +add_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) +{ +#if defined(UTMP_ADD) + if (ptmx < 0) + return true; + if (conf->utmp_helper_path == NULL) + return true; + + char *const argv[] = {conf->utmp_helper_path, UTMP_ADD, getenv("WAYLAND_DISPLAY"), NULL}; + return spawn(reaper, NULL, argv, ptmx, -1, -1, NULL, NULL, NULL) >= 0; +#else + return true; +#endif +} + +static bool +del_utmp_record(const struct config *conf, struct reaper *reaper, int ptmx) +{ +#if defined(UTMP_DEL) + if (ptmx < 0) + return true; + if (conf->utmp_helper_path == NULL) + return true; + + char *del_argument = +#if defined(UTMP_DEL_HAVE_ARGUMENT) + getenv("WAYLAND_DISPLAY") +#else + NULL +#endif + ; + + char *const argv[] = {conf->utmp_helper_path, UTMP_DEL, del_argument, NULL}; + return spawn(reaper, NULL, argv, ptmx, -1, -1, NULL, NULL, NULL) >= 0; +#else + return true; +#endif +} + +#if PTMX_TIMING +static struct timespec last = {0}; +#endif + +static bool cursor_blink_rearm_timer(struct terminal *term); + +/* Externally visible, but not declared in terminal.h, to enable pgo + * to call this function directly */ +bool +fdm_ptmx(struct fdm *fdm, int fd, int events, void *data) +{ + struct terminal *term = data; + + const bool pollin = events & EPOLLIN; + const bool pollout = events & EPOLLOUT; + const bool hup = events & EPOLLHUP; + + if (pollout) { + if (!fdm_ptmx_out(fdm, fd, events, data)) + return false; + } + + /* Prevent blinking while typing */ + if (term->cursor_blink.fd >= 0) { + term->cursor_blink.state = CURSOR_BLINK_ON; + cursor_blink_rearm_timer(term); + } + + if (unlikely(term->interactive_resizing.grid != NULL)) { + /* + * Don't consume PTMX while we're doing an interactive resize, + * since the 'normal' grid we're currently using is a + * temporary one - all changes done to it will be lost when + * the interactive resize ends. + */ + return true; + } + + uint8_t buf[24 * 1024]; + const size_t max_iterations = !hup ? 10 : SIZE_MAX; + + for (size_t i = 0; i < max_iterations && pollin; i++) { + xassert(pollin); + ssize_t count = read(term->ptmx, buf, sizeof(buf)); + + if (count < 0) { + if (errno == EAGAIN || errno == EIO) { + /* + * EAGAIN: no more to read - FDM will trigger us again + * EIO: assume PTY was closed - we already have, or will get, a EPOLLHUP + */ + break; + } + + LOG_ERRNO("failed to read from pseudo terminal"); + return false; + } else if (count == 0) { + /* Reached end-of-file */ + break; + } + + xassert(term->interactive_resizing.grid == NULL); + vt_from_slave(term, buf, count); + + /* Mark inactive tabs as having unread output, so the tab bar + * can show an indicator. Force a tab-bar refresh on the active + * terminal, since the active terminal owns the rendered bar. */ + if (term->window != NULL && term->window->tab_count > 1 && + term != term->window->term) + { + if (!term->has_unread) { + term->has_unread = true; + if (term->window->term != NULL && term->conf->tabs.enabled) + render_refresh_tab_bar(term->window->term); + } + } + } + + if (!term->render.app_sync_updates.enabled) { + /* + * We likely need to re-render. But, we don't want to do it + * immediately. Often, a single client update is done through + * multiple writes. This could lead to us rendering one frame with + * "intermediate" state. + * + * For example, we might end up rendering a frame + * where the client just erased a line, while in the + * next frame, the client wrote to the same line. This + * causes screen "flickering". + * + * Mitigate by always incuring a small delay before + * rendering the next frame. This gives the client + * some time to finish the operation (and thus gives + * us time to receive the last writes before doing any + * actual rendering). + * + * We incur this delay *every* time we receive + * input. To ensure we don't delay rendering + * indefinitely, we start a second timer that is only + * reset when we render. + * + * Note that when the client is producing data at a + * very high pace, we're rate limited by the wayland + * compositor anyway. The delay we introduce here only + * has any effect when the renderer is idle. + */ + uint64_t lower_ns = term->conf->tweak.delayed_render_lower_ns; + uint64_t upper_ns = term->conf->tweak.delayed_render_upper_ns; + + if (lower_ns > 0 && upper_ns > 0) { +#if PTMX_TIMING + struct timespec now; + + clock_gettime(CLOCK_MONOTONIC, &now); + if (last.tv_sec > 0 || last.tv_nsec > 0) { + struct timespec diff; + + timespec_sub(&now, &last, &diff); + LOG_INFO("waited %lds %ldns for more input", + (long)diff.tv_sec, diff.tv_nsec); + } + last = now; +#endif + + xassert(lower_ns < 1000000000); + xassert(upper_ns < 1000000000); + xassert(upper_ns > lower_ns); + + timerfd_settime( + term->delayed_render_timer.lower_fd, 0, + &(struct itimerspec){.it_value = {.tv_nsec = lower_ns}}, + NULL); + + /* Second timeout - only reset when we render. Set to one + * frame (assuming 60Hz) */ + if (!term->delayed_render_timer.is_armed) { + timerfd_settime( + term->delayed_render_timer.upper_fd, 0, + &(struct itimerspec){.it_value = {.tv_nsec = upper_ns}}, + NULL); + term->delayed_render_timer.is_armed = true; + } + } else + render_refresh(term); + } + + if (hup) { + del_utmp_record(term->conf, term->reaper, term->ptmx); + fdm_del(fdm, fd); + term->ptmx = -1; + + /* + * Normally, we do *not* want to shutdown when the PTY is + * closed. Instead, we want to wait for the client application + * to exit. + * + * However, when we're using a pre-existing PTY (the --pty + * option), there _is_ no client application. That is, foot + * does *not* fork+exec anything, and thus the only way to + * shutdown is to wait for the PTY to be closed. + */ + if (term->slave < 0 && !term->conf->hold_at_exit) { + term_shutdown(term); + } + } + + return true; +} + +bool +term_ptmx_pause(struct terminal *term) +{ + if (term->ptmx < 0) + return false; + return fdm_event_del(term->fdm, term->ptmx, EPOLLIN); +} + +bool +term_ptmx_resume(struct terminal *term) +{ + if (term->ptmx < 0) + return false; + return fdm_event_add(term->fdm, term->ptmx, EPOLLIN); +} + +static bool +fdm_flash(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t expiration_count; + ssize_t ret = read( + term->flash.fd, &expiration_count, sizeof(expiration_count)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read flash timer"); + return false; + } + + LOG_DBG("flash timer expired %llu times", + (unsigned long long)expiration_count); + + term->flash.active = false; + render_overlay(term); + + // since the overlay surface is synced with the main window surface, we have + // to commit the main surface for the compositor to acknowledge the new + // overlay state. + wl_surface_commit(term->window->surface.surf); + return true; +} + +static bool +fdm_blink(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t expiration_count; + ssize_t ret = read( + term->blink.fd, &expiration_count, sizeof(expiration_count)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read blink timer"); + return false; + } + + LOG_DBG("blink timer expired %llu times", + (unsigned long long)expiration_count); + + /* Invert blink state */ + term->blink.state = term->blink.state == BLINK_ON + ? BLINK_OFF : BLINK_ON; + + /* Scan all visible cells and mark rows with blinking cells dirty */ + bool no_blinking_cells = true; + for (int r = 0; r < term->rows; r++) { + struct row *row = grid_row_in_view(term->grid, r); + for (int col = 0; col < term->cols; col++) { + struct cell *cell = &row->cells[col]; + + if (cell->attrs.blink) { + cell->attrs.clean = 0; + row->dirty = true; + no_blinking_cells = false; + } + } + } + + if (no_blinking_cells) { + LOG_DBG("disarming blink timer"); + + term->blink.state = BLINK_ON; + fdm_del(term->fdm, term->blink.fd); + term->blink.fd = -1; + } else + render_refresh(term); + return true; +} + +void +term_arm_blink_timer(struct terminal *term) +{ + if (term->blink.fd >= 0) + return; + + LOG_DBG("arming blink timer"); + + int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd < 0) { + LOG_ERRNO("failed to create blink timer FD"); + return; + } + + if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_blink, term)) { + close(fd); + return; + } + + struct itimerspec alarm = { + .it_value = {.tv_sec = 0, .tv_nsec = 500 * 1000000}, + .it_interval = {.tv_sec = 0, .tv_nsec = 500 * 1000000}, + }; + + if (timerfd_settime(fd, 0, &alarm, NULL) < 0) { + LOG_ERRNO("failed to arm blink timer"); + fdm_del(term->fdm, fd); + } + + term->blink.fd = fd; +} + +static void +cursor_refresh(struct terminal *term) +{ + if (!term->window->is_configured) + return; + + term->grid->cur_row->cells[term->grid->cursor.point.col].attrs.clean = 0; + term->grid->cur_row->dirty = true; + render_refresh(term); +} + +static bool +fdm_cursor_blink(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t expiration_count; + ssize_t ret = read( + term->cursor_blink.fd, &expiration_count, sizeof(expiration_count)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read cursor blink timer"); + return false; + } + + LOG_DBG("cursor blink timer expired %llu times", + (unsigned long long)expiration_count); + + /* Invert blink state */ + term->cursor_blink.state = term->cursor_blink.state == CURSOR_BLINK_ON + ? CURSOR_BLINK_OFF : CURSOR_BLINK_ON; + + cursor_refresh(term); + return true; +} + +static bool +fdm_delayed_render(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + + uint64_t unused; + ssize_t ret1 = 0; + ssize_t ret2 = 0; + + if (fd == term->delayed_render_timer.lower_fd) + ret1 = read(term->delayed_render_timer.lower_fd, &unused, sizeof(unused)); + if (fd == term->delayed_render_timer.upper_fd) + ret2 = read(term->delayed_render_timer.upper_fd, &unused, sizeof(unused)); + + if ((ret1 < 0 || ret2 < 0)) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read timeout timer"); + return false; + } + + if (ret1 > 0) + LOG_DBG("lower delay timer expired"); + else if (ret2 > 0) + LOG_DBG("upper delay timer expired"); + + if (ret1 == 0 && ret2 == 0) + return true; + +#if PTMX_TIMING + last = (struct timespec){0}; +#endif + + /* Reset timers */ + struct itimerspec reset = {{0}}; + timerfd_settime(term->delayed_render_timer.lower_fd, 0, &reset, NULL); + timerfd_settime(term->delayed_render_timer.upper_fd, 0, &reset, NULL); + term->delayed_render_timer.is_armed = false; + + render_refresh(term); + return true; +} + +static bool +fdm_app_sync_updates_timeout( + struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t unused; + ssize_t ret = read(term->render.app_sync_updates.timer_fd, + &unused, sizeof(unused)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + LOG_ERRNO("failed to read application synchronized updates timeout timer"); + return false; + } + + term_disable_app_sync_updates(term); + return true; +} + +static bool +fdm_title_update_timeout(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t unused; + ssize_t ret = read(term->render.title.timer_fd, &unused, sizeof(unused)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + LOG_ERRNO("failed to read title update throttle timer"); + return false; + } + + struct itimerspec reset = {{0}}; + timerfd_settime(term->render.title.timer_fd, 0, &reset, NULL); + + render_refresh_title(term); + return true; +} + +static bool +fdm_icon_update_timeout(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t unused; + ssize_t ret = read(term->render.icon.timer_fd, &unused, sizeof(unused)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + LOG_ERRNO("failed to read icon update throttle timer"); + return false; + } + + struct itimerspec reset = {{0}}; + timerfd_settime(term->render.icon.timer_fd, 0, &reset, NULL); + + render_refresh_icon(term); + return true; +} + +static bool +fdm_app_id_update_timeout(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct terminal *term = data; + uint64_t unused; + ssize_t ret = read(term->render.app_id.timer_fd, &unused, sizeof(unused)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + LOG_ERRNO("failed to read app ID update throttle timer"); + return false; + } + + struct itimerspec reset = {{0}}; + timerfd_settime(term->render.app_id.timer_fd, 0, &reset, NULL); + + render_refresh_app_id(term); + return true; +} + +static bool +initialize_render_workers(struct terminal *term) +{ + LOG_INFO("using %hu rendering threads", term->render.workers.count); + + if (sem_init(&term->render.workers.start, 0, 0) < 0 || + sem_init(&term->render.workers.done, 0, 0) < 0) + { + LOG_ERRNO("failed to instantiate render worker semaphores"); + return false; + } + + int err; + if ((err = mtx_init(&term->render.workers.lock, mtx_plain)) != thrd_success) { + LOG_ERR("failed to instantiate render worker mutex: %s (%d)", + thrd_err_as_string(err), err); + goto err_sem_destroy; + } + + mtx_init(&term->render.workers.preapplied_damage.lock, mtx_plain); + cnd_init(&term->render.workers.preapplied_damage.cond); + + term->render.workers.threads = xcalloc( + term->render.workers.count, sizeof(term->render.workers.threads[0])); + + for (size_t i = 0; i < term->render.workers.count; i++) { + struct render_worker_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct render_worker_context) { + .term = term, + .my_id = 1 + i, + }; + + int ret = thrd_create( + &term->render.workers.threads[i], &render_worker_thread, ctx); + if (ret != thrd_success) { + + LOG_ERR("failed to create render worker thread: %s (%d)", + thrd_err_as_string(ret), ret); + term->render.workers.threads[i] = 0; + return false; + } + } + + return true; + +err_sem_destroy: + sem_destroy(&term->render.workers.start); + sem_destroy(&term->render.workers.done); + return false; +} + +static void +free_custom_glyph(struct fcft_glyph **glyph) +{ + if (*glyph == NULL) + return; + + free(pixman_image_get_data((*glyph)->pix)); + pixman_image_unref((*glyph)->pix); + free(*glyph); + *glyph = NULL; +} + +static void +free_custom_glyphs(struct fcft_glyph ***glyphs, size_t count) +{ + if (*glyphs == NULL) + return; + + for (size_t i = 0; i < count; i++) + free_custom_glyph(&(*glyphs)[i]); + + free(*glyphs); + *glyphs = NULL; +} + +static void +term_line_height_update(struct terminal *term) +{ + const struct config *conf = term->conf; + + if (term->conf->line_height.px < 0) { + term->font_line_height.pt = 0; + term->font_line_height.px = -1; + return; + } + + const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + + const float font_original_pt_size = + conf->fonts[0].arr[0].px_size > 0 + ? conf->fonts[0].arr[0].px_size * 72. / dpi + : conf->fonts[0].arr[0].pt_size; + const float font_current_pt_size = + term->font_sizes[0][0].px_size > 0 + ? term->font_sizes[0][0].px_size * 72. / dpi + : term->font_sizes[0][0].pt_size; + + const float change = font_current_pt_size / font_original_pt_size; + const float line_original_pt_size = conf->line_height.px > 0 + ? conf->line_height.px * 72. / dpi + : conf->line_height.pt; + + term->font_line_height.px = 0; + term->font_line_height.pt = fmaxf(line_original_pt_size * change, 0.); +} + +static bool +term_set_fonts(struct terminal *term, struct fcft_font *fonts[static 4], + bool resize_grid) +{ + for (size_t i = 0; i < 4; i++) { + xassert(fonts[i] != NULL); + + fcft_destroy(term->fonts[i]); + term->fonts[i] = fonts[i]; + } + + free_custom_glyphs( + &term->custom_glyphs.box_drawing, GLYPH_BOX_DRAWING_COUNT); + free_custom_glyphs( + &term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT); + free_custom_glyphs( + &term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); + free_custom_glyphs( + &term->custom_glyphs.octants, GLYPH_OCTANTS_COUNT); + + const struct config *conf = term->conf; + + const struct fcft_glyph *M = fcft_rasterize_char_utf32( + fonts[0], U'M', term->font_subpixel); + int advance = M != NULL ? M->advance.x : term->fonts[0]->max_advance.x; + + term_line_height_update(term); + + term->cell_width = advance + + term_pt_or_px_as_pixels(term, &conf->letter_spacing); + + term->cell_height = term->font_line_height.px >= 0 + ? term_pt_or_px_as_pixels(term, &term->font_line_height) + : max(term->fonts[0]->height, + term->fonts[0]->ascent + term->fonts[0]->descent); + + if (term->cell_width <= 0) + term->cell_width = 1; + if (term->cell_height <= 0) + term->cell_height = 1; + + term->font_x_ofs = term_pt_or_px_as_pixels(term, &conf->horizontal_letter_offset); + term->font_y_ofs = term_pt_or_px_as_pixels(term, &conf->vertical_letter_offset); + + term->font_baseline = term_font_baseline(term); + + LOG_INFO("cell width=%d, height=%d", term->cell_width, term->cell_height); + + sixel_cell_size_changed(term); + + /* Optimization - some code paths (are forced to) call + * render_resize() after this function */ + if (resize_grid) { + /* Use force, since cell-width/height may have changed */ + enum resize_options resize_opts = RESIZE_FORCE; + if (conf->resize_keep_grid) + resize_opts |= RESIZE_KEEP_GRID; + + render_resize( + term, + (int)roundf(term->width / term->scale), + (int)roundf(term->height / term->scale), + resize_opts); + } + return true; +} + +static float +get_font_dpi(const struct terminal *term) +{ + /* + * Use output's DPI to scale font. This is to ensure the font has + * the same physical height (if measured by a ruler) regardless of + * monitor. + * + * Conceptually, we use the physical monitor specs to calculate + * the DPI, and we ignore the output's scaling factor. + * + * However, to deal with legacy fractional scaling, where we're + * told to render at e.g. 2x, but are then downscaled by the + * compositor to e.g. 1.25, we use the scaled DPI value multiplied + * by the scale factor instead. + * + * For integral scaling factors the resulting DPI is the same as + * if we had used the physical DPI. + * + * For legacy fractional scaling factors we'll get a DPI *larger* + * than the physical DPI, that ends up being right when later + * downscaled by the compositor. + * + * With the newer fractional-scale-v1 protocol, we use the + * monitor's real DPI, since we scale everything to the correct + * scaling factor (no downscaling done by the compositor). + */ + + const struct wl_window *win = term->window; + const struct monitor *mon = NULL; + + if (tll_length(win->on_outputs) > 0) + mon = tll_back(win->on_outputs); + else { + if (term->font_dpi_before_unmap > 0.) { + /* + * Use last known "good" DPI + * + * This avoids flickering when window is unmapped/mapped + * (some compositors do this when a window is minimized), + * on a multi-monitor setup with different monitor DPIs. + */ + return term->font_dpi_before_unmap; + } + + if (tll_length(term->wl->monitors) > 0) + mon = &tll_front(term->wl->monitors); + } + + const float monitor_dpi = mon != NULL + ? term_fractional_scaling(term) + ? mon->dpi.physical + : mon->dpi.scaled + : 96.; + + return monitor_dpi > 0. ? monitor_dpi : 96.; +} + +static enum fcft_subpixel +get_font_subpixel(const struct terminal *term) +{ + if (term->colors.alpha != 0xffff) { + /* Can't do subpixel rendering on transparent background */ + return FCFT_SUBPIXEL_NONE; + } + + enum wl_output_subpixel wl_subpixel; + + /* + * Wayland doesn't tell us *which* part of the surface that goes + * on a specific output, only whether the surface is mapped to an + * output or not. + * + * Thus, when determining which subpixel mode to use, we can't do + * much but select *an* output. So, we pick the one we were most + * recently mapped on. + * + * If we're not mapped at all, we pick the first available + * monitor, and hope that's where we'll eventually get mapped. + * + * If there aren't any monitors we use the "default" subpixel + * mode. + */ + + if (tll_length(term->window->on_outputs) > 0) + wl_subpixel = tll_back(term->window->on_outputs)->subpixel; + else if (tll_length(term->wl->monitors) > 0) + wl_subpixel = tll_front(term->wl->monitors).subpixel; + else + wl_subpixel = WL_OUTPUT_SUBPIXEL_UNKNOWN; + + switch (wl_subpixel) { + case WL_OUTPUT_SUBPIXEL_UNKNOWN: return FCFT_SUBPIXEL_DEFAULT; + case WL_OUTPUT_SUBPIXEL_NONE: return FCFT_SUBPIXEL_NONE; + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB: return FCFT_SUBPIXEL_HORIZONTAL_RGB; + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR: return FCFT_SUBPIXEL_HORIZONTAL_BGR; + case WL_OUTPUT_SUBPIXEL_VERTICAL_RGB: return FCFT_SUBPIXEL_VERTICAL_RGB; + case WL_OUTPUT_SUBPIXEL_VERTICAL_BGR: return FCFT_SUBPIXEL_VERTICAL_BGR; + } + + return FCFT_SUBPIXEL_DEFAULT; +} + +int +term_pt_or_px_as_pixels(const struct terminal *term, + const struct pt_or_px *pt_or_px) +{ + float scale = !term->font_is_sized_by_dpi ? term->scale : 1.; + float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + + return pt_or_px->px == 0 + ? (int)roundf(pt_or_px->pt * scale * dpi / 72) + : (int)roundf(pt_or_px->px * scale); +} + +struct font_load_data { + size_t count; + const char **names; + const char *attrs; + + const struct fcft_font_options *options; + struct fcft_font **font; +}; + +static int +font_loader_thread(void *_data) +{ + struct font_load_data *data = _data; + *data->font = fcft_from_name2( + data->count, data->names, data->attrs, data->options); + return *data->font != NULL; +} + +static bool +reload_fonts(struct terminal *term, bool resize_grid) +{ + const struct config *conf = term->conf; + + const size_t counts[4] = { + conf->fonts[0].count, + conf->fonts[1].count, + conf->fonts[2].count, + conf->fonts[3].count, + }; + + /* Configure size (which may have been changed run-time) */ + char **names[4]; + for (size_t i = 0; i < 4; i++) { + names[i] = xmalloc(counts[i] * sizeof(names[i][0])); + + const struct config_font_list *font_list = &conf->fonts[i]; + + for (size_t j = 0; j < font_list->count; j++) { + const struct config_font *font = &font_list->arr[j]; + bool use_px_size = term->font_sizes[i][j].px_size > 0; + char size[64]; + + const float scale = term->font_is_sized_by_dpi ? 1. : term->scale; + + if (use_px_size) + snprintf(size, sizeof(size), ":pixelsize=%d", + (int)roundf(term->font_sizes[i][j].px_size * scale)); + else + snprintf(size, sizeof(size), ":size=%.2f", + term->font_sizes[i][j].pt_size * scale); + + names[i][j] = xstrjoin(font->pattern, size); + } + } + + /* Did user configure custom bold/italic fonts? + * Or should we use the regular font, with weight/slant attributes? */ + const bool custom_bold = counts[1] > 0; + const bool custom_italic = counts[2] > 0; + const bool custom_bold_italic = counts[3] > 0; + + const size_t count_regular = counts[0]; + const char **names_regular = (const char **)names[0]; + + const size_t count_bold = custom_bold ? counts[1] : counts[0]; + const char **names_bold = (const char **)(custom_bold ? names[1] : names[0]); + + const size_t count_italic = custom_italic ? counts[2] : counts[0]; + const char **names_italic = (const char **)(custom_italic ? names[2] : names[0]); + + const size_t count_bold_italic = custom_bold_italic ? counts[3] : counts[0]; + const char **names_bold_italic = (const char **)(custom_bold_italic ? names[3] : names[0]); + + const bool use_dpi = term->font_is_sized_by_dpi; + char *dpi = xasprintf("dpi=%.2f", use_dpi ? term->font_dpi : 96.); + + char *attrs[4] = { + [0] = dpi, /* Takes ownership */ + [1] = xstrjoin(dpi, !custom_bold ? ":weight=bold" : ""), + [2] = xstrjoin(dpi, !custom_italic ? ":slant=italic" : ""), + [3] = xstrjoin(dpi, !custom_bold_italic ? ":weight=bold:slant=italic" : ""), + }; + + struct fcft_font_options *options = fcft_font_options_create(); + + options->scaling_filter = conf->tweak.fcft_filter; + options->color_glyphs.format = PIXMAN_a8r8g8b8; + options->color_glyphs.srgb_decode = + wayl_do_linear_blending(term->wl, term->conf); + + if (shm_chain_bit_depth(term->render.chains.grid) >= SHM_BITS_10) { + /* + * Use a high-res buffer type for emojis. We don't want to use + * an a2r10g0b10 type of surface, since we need more than 2 + * bits for alpha. + */ +#if defined(HAVE_PIXMAN_RGBA_16) + options->color_glyphs.format = PIXMAN_a16b16g16r16; +#else + options->color_glyphs.format = PIXMAN_rgba_float; +#endif + } + + struct fcft_font *fonts[4]; + struct font_load_data data[4] = { + {count_regular, names_regular, attrs[0], options, &fonts[0]}, + {count_bold, names_bold, attrs[1], options, &fonts[1]}, + {count_italic, names_italic, attrs[2], options, &fonts[2]}, + {count_bold_italic, names_bold_italic, attrs[3], options, &fonts[3]}, + }; + + thrd_t tids[4] = {0}; + for (size_t i = 0; i < 4; i++) { + int ret = thrd_create(&tids[i], &font_loader_thread, &data[i]); + if (ret != thrd_success) { + LOG_ERR("failed to create font loader thread: %s (%d)", + thrd_err_as_string(ret), ret); + break; + } + } + + bool success = true; + for (size_t i = 0; i < 4; i++) { + if (tids[i] != 0) { + int ret; + if (thrd_join(tids[i], &ret) != thrd_success) + success = false; + else + success = success && ret; + } else + success = false; + } + + fcft_font_options_destroy(options); + + for (size_t i = 0; i < 4; i++) { + for (size_t j = 0; j < counts[i]; j++) + free(names[i][j]); + free(names[i]); + free(attrs[i]); + } + + if (!success) { + LOG_ERR("failed to load primary fonts"); + for (size_t i = 0; i < 4; i++) { + fcft_destroy(fonts[i]); + fonts[i] = NULL; + } + } + + return success ? term_set_fonts(term, fonts, resize_grid) : success; +} + +static bool +load_fonts_from_conf(struct terminal *term) +{ + const struct config *conf = term->conf; + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + + for (size_t j = 0; j < font_list->count; j++) { + const struct config_font *font = &font_list->arr[j]; + term->font_sizes[i][j] = (struct config_font){ + .pt_size = font->pt_size, .px_size = font->px_size}; + } + } + + return reload_fonts(term, true); +} + +static void fdm_client_terminated( + struct reaper *reaper, pid_t pid, int status, void *data); + +static const int PTY_OPEN_FLAGS = O_RDWR | O_NOCTTY; + +struct terminal * +term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, + struct wayland *wayl, const char *foot_exe, const char *cwd, + const char *token, const char *pty_path, + int argc, char *const *argv, const char *const *envp, + void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data) +{ + int ptmx = -1; + int flash_fd = -1; + int delay_lower_fd = -1; + int delay_upper_fd = -1; + int app_sync_updates_fd = -1; + int title_update_fd = -1; + int icon_update_fd = -1; + int app_id_update_fd = -1; + + struct terminal *term = malloc(sizeof(*term)); + if (unlikely(term == NULL)) { + LOG_ERRNO("malloc() failed"); + return NULL; + } + + ptmx = pty_path ? open(pty_path, PTY_OPEN_FLAGS) : posix_openpt(PTY_OPEN_FLAGS); + if (ptmx < 0) { + LOG_ERRNO("failed to open PTY"); + goto close_fds; + } + if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) { + LOG_ERRNO("failed to create flash timer FD"); + goto close_fds; + } + if ((delay_lower_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (delay_upper_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) + { + LOG_ERRNO("failed to create delayed rendering timer FDs"); + goto close_fds; + } + + if ((app_sync_updates_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) + { + LOG_ERRNO("failed to create application synchronized updates timer FD"); + goto close_fds; + } + + if ((title_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) + { + LOG_ERRNO("failed to create title update throttle timer FD"); + goto close_fds; + } + + if ((icon_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) + { + LOG_ERRNO("failed to create icon update throttle timer FD"); + goto close_fds; + } + + if ((app_id_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) + { + LOG_ERRNO("failed to create app ID update throttle timer FD"); + goto close_fds; + } + + if (ioctl(ptmx, (unsigned int)TIOCSWINSZ, + &(struct winsize){.ws_row = 24, .ws_col = 80}) < 0) + { + LOG_ERRNO("failed to set initial TIOCSWINSZ"); + goto close_fds; + } + + /* Need to register *very* early (before the first "goto err"), to + * ensure term_destroy() doesn't unref a key-binding we haven't + * yet ref:d */ + key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf); + + int ptmx_flags; + if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 || + fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0) + { + LOG_ERRNO("failed to configure ptmx as non-blocking"); + goto err; + } + + /* + * Enable all FDM callbackes *except* ptmx - we can't do that + * until the window has been 'configured' since we don't have a + * size (and thus no grid) before then. + */ + + if (!fdm_add(fdm, flash_fd, EPOLLIN, &fdm_flash, term) || + !fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) || + !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) || + !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout, term) || + !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term) || + !fdm_add(fdm, icon_update_fd, EPOLLIN, &fdm_icon_update_timeout, term) || + !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, term)) + { + goto err; + } + + const enum shm_bit_depth desired_bit_depth = + conf->tweak.surface_bit_depth == SHM_BITS_AUTO + ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_16 : SHM_BITS_8 + : conf->tweak.surface_bit_depth; + + const struct color_theme *theme = NULL; + switch (conf->initial_color_theme) { + case COLOR_THEME_DARK: theme = &conf->colors_dark; break; + case COLOR_THEME_LIGHT: theme = &conf->colors_light; break; + case COLOR_THEME_1: BUG("COLOR_THEME_1 should not be used"); break; + case COLOR_THEME_2: BUG("COLOR_THEME_2 should not be used"); break; + } + + /* Initialize configure-based terminal attributes */ + *term = (struct terminal) { + .fdm = fdm, + .reaper = reaper, + .conf = conf, + .slave = -1, + .ptmx = ptmx, + .ptmx_buffers = tll_init(), + .ptmx_paste_buffers = tll_init(), + .font_sizes = { + xmalloc(sizeof(term->font_sizes[0][0]) * conf->fonts[0].count), + xmalloc(sizeof(term->font_sizes[1][0]) * conf->fonts[1].count), + xmalloc(sizeof(term->font_sizes[2][0]) * conf->fonts[2].count), + xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count), + }, + .font_dpi = 0., + .font_dpi_before_unmap = -1., + .font_subpixel = (theme->alpha == 0xffff /* Can't do subpixel rendering on transparent background */ + ? FCFT_SUBPIXEL_DEFAULT + : FCFT_SUBPIXEL_NONE), + .cursor_keys_mode = CURSOR_KEYS_NORMAL, + .keypad_keys_mode = KEYPAD_NUMERICAL, + .reverse_wrap = true, + .auto_margin = true, + .window_title_stack = tll_init(), + .scale = 1., + .scale_before_unmap = -1, + .flash = {.fd = flash_fd}, + .blink = {.fd = -1}, + .vt = { + .state = 0, /* STATE_GROUND */ + }, + .colors = { + .fg = theme->fg, + .bg = theme->bg, + .alpha = theme->alpha, + .cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text, + .cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor, + .selection_fg = theme->selection_fg, + .selection_bg = theme->selection_bg, + .active_theme = conf->initial_color_theme, + }, + .color_stack = { + .stack = NULL, + .size = 0, + .idx = 0, + }, + .origin = ORIGIN_ABSOLUTE, + .cursor_style = conf->cursor.style, + .cursor_blink = { + .decset = false, + .deccsusr = conf->cursor.blink.enabled, + .state = CURSOR_BLINK_ON, + .fd = -1, + }, + .selection = { + .coords = { + .start = {-1, -1}, + .end = {-1, -1}, + }, + .pivot = { + .start = {-1, -1}, + .end = {-1, -1}, + }, + .auto_scroll = { + .fd = -1, + }, + }, + .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, + .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, + .grid = &term->normal, + .composed = NULL, + .alt_scrolling = conf->mouse.alternate_scroll_mode, + .meta = { + .esc_prefix = true, + .eight_bit = true, + }, + .num_lock_modifier = true, + .bell_action_enabled = true, + .tab_stops = tll_init(), + .wl = wayl, + .render = { + .chains = { + .grid = shm_chain_new(wayl, true, 1 + conf->render_worker_count, + desired_bit_depth, &render_buffer_release_callback, term), + .search = shm_chain_new(wayl, false, 1 ,desired_bit_depth, NULL, NULL), + .scrollback_indicator = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .render_timer = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .url = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .csd = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .overlay = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .tab_bar = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .tab_overview = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + }, + .scrollback_lines = conf->scrollback.lines, + .app_sync_updates.timer_fd = app_sync_updates_fd, + .title = { + .timer_fd = title_update_fd, + }, + .icon = { + .timer_fd = icon_update_fd, + }, + .app_id = { + .timer_fd = app_id_update_fd, + }, + .workers = { + .count = conf->render_worker_count, + .queue = tll_init(), + }, + }, + .delayed_render_timer = { + .is_armed = false, + .lower_fd = delay_lower_fd, + .upper_fd = delay_upper_fd, + }, + .sixel = { + .scrolling = true, + .use_private_palette = true, + .palette_size = SIXEL_MAX_COLORS, + .max_width = SIXEL_MAX_WIDTH, + .max_height = SIXEL_MAX_HEIGHT, + }, + .shutdown = { + .terminate_timeout_fd = -1, + .cb = shutdown_cb, + .cb_data = shutdown_data, + }, + .foot_exe = xstrdup(foot_exe), + .cwd = xstrdup(cwd), + .grapheme_shaping = conf->tweak.grapheme_shaping, +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + .ime_enabled = true, +#endif + .active_notifications = tll_init(), + }; + + pixman_region32_init(&term->render.last_overlay_clip); + + term_update_ascii_printer(term); + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + for (size_t j = 0; j < font_list->count; j++) { + const struct config_font *font = &font_list->arr[j]; + term->font_sizes[i][j] = (struct config_font){ + .pt_size = font->pt_size, .px_size = font->px_size}; + } + } + + for (size_t i = 0; i < ALEN(term->notification_icons); i++) { + term->notification_icons[i].tmp_file_fd = -1; + } + + add_utmp_record(conf, reaper, ptmx); + + if (!pty_path) { + /* Start the slave/client */ + if ((term->slave = slave_spawn( + term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, + conf->term, conf->shell, conf->login_shell, + &conf->notifications)) == -1) + { + goto err; + } + + reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); + } + + /* Guess scale; we're not mapped yet, so we don't know on which + * output we'll be. Use scaling factor from first monitor */ + xassert(tll_length(term->wl->monitors) > 0); + term->scale = tll_front(term->wl->monitors).scale; + + /* Initialize the Wayland window backend */ + if ((term->window = wayl_win_init(term, token)) == NULL) + goto err; + + /* Load fonts */ + if (!term_font_dpi_changed(term, 0.)) + goto err; + + term->font_subpixel = get_font_subpixel(term); + + term_set_window_title(term, conf->title); + + /* Let the Wayland backend know we exist */ + tll_push_back(wayl->terms, term); + + switch (conf->startup_mode) { + case STARTUP_WINDOWED: + break; + + case STARTUP_MAXIMIZED: + xdg_toplevel_set_maximized(term->window->xdg_toplevel); + break; + + case STARTUP_FULLSCREEN: + xdg_toplevel_set_fullscreen(term->window->xdg_toplevel, NULL); + break; + } + + if (!initialize_render_workers(term)) + goto err; + + return term; + +err: + term->shutdown.in_progress = true; + term_destroy(term); + return NULL; + +close_fds: + close(ptmx); + fdm_del(fdm, flash_fd); + fdm_del(fdm, delay_lower_fd); + fdm_del(fdm, delay_upper_fd); + fdm_del(fdm, app_sync_updates_fd); + fdm_del(fdm, title_update_fd); + fdm_del(fdm, icon_update_fd); + fdm_del(fdm, app_id_update_fd); + + free(term); + return NULL; +} + +struct terminal * +term_tab_new(struct terminal *primary, + int argc, char *const *argv, const char *const *envp, + void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data) +{ + struct wl_window *win = primary->window; + + if (win->tab_count >= TAB_MAX) { + LOG_ERR("maximum number of tabs (%d) reached", TAB_MAX); + return NULL; + } + + const struct config *conf = primary->conf; + struct fdm *fdm = primary->fdm; + struct reaper *reaper = primary->reaper; + struct wayland *wayl = primary->wl; + + int ptmx = -1; + int flash_fd = -1; + int delay_lower_fd = -1; + int delay_upper_fd = -1; + int app_sync_updates_fd = -1; + int title_update_fd = -1; + int icon_update_fd = -1; + int app_id_update_fd = -1; + + struct terminal *term = malloc(sizeof(*term)); + if (unlikely(term == NULL)) { + LOG_ERRNO("malloc() failed"); + return NULL; + } + + ptmx = posix_openpt(PTY_OPEN_FLAGS); + if (ptmx < 0) { + LOG_ERRNO("failed to open PTY for new tab"); + goto close_fds; + } + if ((flash_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (delay_lower_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (delay_upper_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (app_sync_updates_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (title_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (icon_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0 || + (app_id_update_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK)) < 0) + { + LOG_ERRNO("failed to create timer FDs for new tab"); + goto close_fds; + } + + if (ioctl(ptmx, (unsigned int)TIOCSWINSZ, + &(struct winsize){ + .ws_row = (unsigned short)primary->rows, + .ws_col = (unsigned short)primary->cols}) < 0) + { + LOG_ERRNO("failed to set TIOCSWINSZ for new tab"); + goto close_fds; + } + + key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf); + + int ptmx_flags; + if ((ptmx_flags = fcntl(ptmx, F_GETFL)) < 0 || + fcntl(ptmx, F_SETFL, ptmx_flags | O_NONBLOCK) < 0) + { + LOG_ERRNO("failed to configure ptmx as non-blocking"); + goto err; + } + + const enum shm_bit_depth desired_bit_depth = + conf->tweak.surface_bit_depth == SHM_BITS_AUTO + ? wayl_do_linear_blending(wayl, conf) ? SHM_BITS_16 : SHM_BITS_8 + : conf->tweak.surface_bit_depth; + + const struct color_theme *theme = NULL; + switch (conf->initial_color_theme) { + case COLOR_THEME_DARK: theme = &conf->colors_dark; break; + case COLOR_THEME_LIGHT: theme = &conf->colors_light; break; + case COLOR_THEME_1: BUG("COLOR_THEME_1 should not be used"); break; + case COLOR_THEME_2: BUG("COLOR_THEME_2 should not be used"); break; + } + + *term = (struct terminal){ + .fdm = fdm, + .reaper = reaper, + .conf = conf, + .is_tab = true, + .slave = -1, + .ptmx = ptmx, + .ptmx_buffers = tll_init(), + .ptmx_paste_buffers = tll_init(), + .font_sizes = { + xmalloc(sizeof(term->font_sizes[0][0]) * conf->fonts[0].count), + xmalloc(sizeof(term->font_sizes[1][0]) * conf->fonts[1].count), + xmalloc(sizeof(term->font_sizes[2][0]) * conf->fonts[2].count), + xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count), + }, + .font_dpi = 0., + .font_dpi_before_unmap = -1., + .font_subpixel = (theme->alpha == 0xffff + ? FCFT_SUBPIXEL_DEFAULT + : FCFT_SUBPIXEL_NONE), + .cursor_keys_mode = CURSOR_KEYS_NORMAL, + .keypad_keys_mode = KEYPAD_NUMERICAL, + .reverse_wrap = true, + .auto_margin = true, + .window_title_stack = tll_init(), + .scale = primary->scale, + .scale_before_unmap = -1, + .flash = {.fd = flash_fd}, + .blink = {.fd = -1}, + .vt = {.state = 0}, + .colors = { + .fg = theme->fg, + .bg = theme->bg, + .alpha = theme->alpha, + .cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text, + .cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor, + .selection_fg = theme->selection_fg, + .selection_bg = theme->selection_bg, + .active_theme = conf->initial_color_theme, + }, + .color_stack = {.stack = NULL, .size = 0, .idx = 0}, + .origin = ORIGIN_ABSOLUTE, + .cursor_style = conf->cursor.style, + .cursor_blink = { + .decset = false, + .deccsusr = conf->cursor.blink.enabled, + .state = CURSOR_BLINK_ON, + .fd = -1, + }, + .selection = { + .coords = {.start = {-1, -1}, .end = {-1, -1}}, + .pivot = {.start = {-1, -1}, .end = {-1, -1}}, + .auto_scroll = {.fd = -1}, + }, + .normal = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, + .alt = {.scroll_damage = tll_init(), .sixel_images = tll_init()}, + .grid = &term->normal, + .composed = NULL, + .alt_scrolling = conf->mouse.alternate_scroll_mode, + .meta = {.esc_prefix = true, .eight_bit = true}, + .num_lock_modifier = true, + .bell_action_enabled = true, + .tab_stops = tll_init(), + .wl = wayl, + .window = win, + .render = { + .chains = { + .grid = shm_chain_new(wayl, true, 1 + conf->render_worker_count, + desired_bit_depth, &render_buffer_release_callback, term), + .search = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .scrollback_indicator = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .render_timer = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .url = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .csd = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .overlay = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .tab_bar = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + .tab_overview = shm_chain_new(wayl, false, 1, desired_bit_depth, NULL, NULL), + }, + .scrollback_lines = conf->scrollback.lines, + .app_sync_updates.timer_fd = app_sync_updates_fd, + .title = {.timer_fd = title_update_fd}, + .icon = {.timer_fd = icon_update_fd}, + .app_id = {.timer_fd = app_id_update_fd}, + .workers = { + .count = conf->render_worker_count, + .queue = tll_init(), + }, + }, + .delayed_render_timer = { + .is_armed = false, + .lower_fd = delay_lower_fd, + .upper_fd = delay_upper_fd, + }, + .sixel = { + .scrolling = true, + .use_private_palette = true, + .palette_size = SIXEL_MAX_COLORS, + .max_width = SIXEL_MAX_WIDTH, + .max_height = SIXEL_MAX_HEIGHT, + }, + .shutdown = { + .terminate_timeout_fd = -1, + .cb = shutdown_cb, + .cb_data = shutdown_data, + }, + .foot_exe = xstrdup(primary->foot_exe), + .cwd = xstrdup( + conf->tabs.inherit_cwd + ? win->term->cwd + : (getenv("HOME") != NULL ? getenv("HOME") : "/")), + .grapheme_shaping = conf->tweak.grapheme_shaping, +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + .ime_enabled = true, +#endif + .active_notifications = tll_init(), + }; + + pixman_region32_init(&term->render.last_overlay_clip); + term_update_ascii_printer(term); + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); + + /* Inherit font sizes from primary so the new tab matches its zoom level */ + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + for (size_t j = 0; j < font_list->count; j++) + term->font_sizes[i][j] = primary->font_sizes[i][j]; + } + + for (size_t i = 0; i < ALEN(term->notification_icons); i++) + term->notification_icons[i].tmp_file_fd = -1; + + if (!fdm_add(fdm, flash_fd, EPOLLIN, &fdm_flash, term) || + !fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) || + !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) || + !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout, term) || + !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term) || + !fdm_add(fdm, icon_update_fd, EPOLLIN, &fdm_icon_update_timeout, term) || + !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, term)) + { + goto err; + } + + add_utmp_record(conf, reaper, ptmx); + + if ((term->slave = slave_spawn( + term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, + conf->term, conf->shell, conf->login_shell, + &conf->notifications)) == -1) + { + goto err; + } + + reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); + + /* Load fonts (uses primary's scale) */ + if (!term_font_dpi_changed(term, 0.)) + goto err; + + term->font_subpixel = get_font_subpixel(term); + + /* Resize grid to match current window dimensions. cell_width and + * cell_height were already computed by term_set_fonts above, using the + * inherited font_sizes — inheriting primary's cell geometry would mask + * that, since primary may be mid-zoom or mid-reload. */ + term->width = primary->width; + term->height = primary->height; + + tll_push_back(wayl->terms, term); + + /* Register in window's tab list */ + win->tabs[win->tab_count++] = term; + + /* Enable ptmx I/O now (window is already configured) */ + fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); + + if (!initialize_render_workers(term)) + goto err; + + /* Switch to the new tab */ + term_tab_switch(win, win->tab_count - 1); + + return term; + +err: + term->shutdown.in_progress = true; + term_destroy(term); + return NULL; + +close_fds: + if (ptmx >= 0) close(ptmx); + fdm_del(fdm, flash_fd); + fdm_del(fdm, delay_lower_fd); + fdm_del(fdm, delay_upper_fd); + fdm_del(fdm, app_sync_updates_fd); + fdm_del(fdm, title_update_fd); + fdm_del(fdm, icon_update_fd); + fdm_del(fdm, app_id_update_fd); + free(term); + return NULL; +} + +void +term_tab_switch(struct wl_window *win, size_t idx) +{ + if (idx >= win->tab_count) + return; + + struct terminal *prev = win->term; + /* render_resize expects logical (unscaled) sizes; prev->width/height + * are physical pixels. Convert back to logical so render_resize's + * internal scale multiplication doesn't double-apply it. */ + int cur_width = (int)roundf(prev->width / prev->scale); + int cur_height = (int)roundf(prev->height / prev->scale); + + win->active_tab = idx; + win->term = win->tabs[idx]; + + struct terminal *term = win->term; + + /* The newly active tab is being looked at — clear its unread flag */ + term->has_unread = false; + + /* Inherit the surface kind so cursor updates work without a pointer re-enter */ + term->active_surface = prev->active_surface; + + /* Update keyboard and mouse focus for all seats looking at this window. + * Do this before term_kbd_focus_out(prev) so its guard (checking whether + * any seat still points to it) sees that no seat does. */ + tll_foreach(term->wl->seats, it) { + struct seat *seat = &it->item; + if (seat->kbd_focus != NULL && seat->kbd_focus->window == win) + seat->kbd_focus = term; + if (seat->mouse_focus != NULL && seat->mouse_focus->window == win) + seat->mouse_focus = term; + } + + /* prev loses keyboard focus now that no seat points to it */ + term_kbd_focus_out(prev); + + bool has_kbd_focus = false; + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) { + has_kbd_focus = true; + break; + } + } + if (has_kbd_focus) + term_visual_focus_in(term); + else + term_visual_focus_out(term); + + /* Scale changes (fractional-scale-v1, preferred_buffer_scale, output + * enter/leave) only notify win->term — inactive tabs miss them. Catch + * the incoming tab up on scale, DPI, and font size before we resize. */ + const float old_scale = term->scale; + term_update_scale(term); + term_font_dpi_changed(term, old_scale); + term_font_subpixel_changed(term); + + /* Cancel the outgoing tab's in-flight frame_callback. Its listener data + * points at prev, so when it fires it services prev's empty pending bits + * and nothing gets drawn. Dropping it lets fdm_hook_refresh_pending_terminals + * take the immediate-render path for the new active tab. */ + if (win->frame_callback != NULL) { + wl_callback_destroy(win->frame_callback); + win->frame_callback = NULL; + } + + /* Use current window dimensions, not the inactive tab's stale ones. + * render_resize allocates the grid rows — term_kbd_focus_in must come + * after this because cursor_refresh dereferences cur_row. */ + render_resize(term, cur_width, cur_height, RESIZE_FORCE); + + if (has_kbd_focus) + term_kbd_focus_in(term); + + render_refresh_csd(term); + render_refresh_tab_bar(term); + term_xcursor_update(term); +} + +void +term_tab_close(struct terminal *term) +{ + term_shutdown(term); +} + +size_t +term_tab_bar_hit_test(const struct terminal *term, int x, int y) +{ + const struct wl_window *win = term->window; + if (win->tab_bar.sub == NULL) + return SIZE_MAX; + + const size_t n = win->tab_layout.count; + if (n == 0) + return SIZE_MAX; + + if (y < win->tab_layout.y || y >= win->tab_layout.y + win->tab_layout.h) + return SIZE_MAX; + + for (size_t i = 0; i < n; i++) { + const int tx = win->tab_layout.xs[i]; + const int tw = win->tab_layout.ws[i]; + if (x >= tx && x < tx + tw) + return i; + } + return SIZE_MAX; +} + +void +term_window_configured(struct terminal *term) +{ + /* Enable ptmx FDM callback */ + if (!term->shutdown.in_progress) { + xassert(term->window->is_configured); + fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); + + const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); + LOG_INFO("gamma-correct blending: %s", gamma_correct ? "enabled" : "disabled"); + } +} + +/* + * Shutdown logic + * + * A foot instance can be terminated in two ways: + * + * - the client application terminates (user types 'exit', or pressed C-d in the + * shell, etc) + * - the foot window is closed + * + * Both variants need to trigger to "other" action. I.e. if the client + * application is terminated, then we need to close the window. If the window is + * closed, we need to terminate the client application. + * + * Only when *both* tasks have completed do we consider ourselves fully + * shutdown. This is when we can call term_destroy(), and the user provided + * shutdown callback. + * + * The functions involved with this are: + * + * - shutdown_maybe_done(): called after any of the two tasks above have + * completed. When it determines that *both* tasks are done, it calls + * term_destroy() and the user provided shutdown callback. + * + * - fdm_client_terminated(): reaper callback, called when the client + * application has terminated. + * + * + Kills the "terminate" timeout timer + * + Calls shutdown_maybe_done() if the shutdown procedure has already + * started (i.e. the window being closed initiated the shutdown) + * -OR- + * Initiates the shutdown itself, by calling term_shutdown() (client + * application termination initiated the shutdown). + * + * - term_shutdown(): unregisters all FDM callbacks, sends SIGTERM to the client + * application and installs a "terminate" timeout timer (if it hasn't already + * terminated). Finally registers an event FD with the FDM, which is + * immediately triggered. This is done to ensure any pending FDM events are + * handled before shutting down. + * + * - fdm_shutdown(): FDM callback, triggered by the event FD in + * term_shutdown(). Unmaps and destroys the window resources, and ensures the + * seats' focused pointers don't reference us. Finally calls + * shutdown_maybe_done(). + * + * - fdm_terminate_timeout(): FDM callback for the "terminate" timeout + * timer. This function is called when the client application hasn't + * terminated after 60 seconds (after the SIGTERM). Sends SIGKILL to the + * client application. + * + * - term_destroy(): normally called from shutdown_maybe_done(), when both the + * window has been unmapped, and the client application has terminated. In + * this case, it simply destroys all resources. + * + * It may however also be called without term_shutdown() having been called + * (typically in error code paths - for example, when the Wayland connection + * is closed by the compositor). In this case, the client application is + * typically still running, and we can't assume the FDM is running. To handle + * this, we install configure a 60 second SIGALRM, send SIGTERM to the client + * application, and then enter a blocking waitpid(). + * + * If the alarm triggers, we send SIGKILL and once again enter a blocking + * waitpid(). + */ + +static void +shutdown_maybe_done(struct terminal *term) +{ + bool shutdown_done = + term->shutdown.fdm_done && term->shutdown.client_has_terminated; + + LOG_DBG("fdm_done=%d, slave-has-been-reaped=%d --> %s", + term->shutdown.fdm_done, term->shutdown.client_has_terminated, + (shutdown_done + ? "shutdown done, calling term_destroy()" + : "no action")); + + if (!shutdown_done) + return; + + void (*cb)(void *, int) = term->shutdown.cb; + void *cb_data = term->shutdown.cb_data; + + int exit_code = term_destroy(term); + if (cb != NULL) + cb(cb_data, exit_code); +} + +static void +fdm_client_terminated(struct reaper *reaper, pid_t pid, int status, void *data) +{ + struct terminal *term = data; + LOG_DBG("slave (PID=%u) died", pid); + + term->shutdown.client_has_terminated = true; + term->shutdown.exit_status = status; + + if (term->shutdown.terminate_timeout_fd >= 0) { + fdm_del(term->fdm, term->shutdown.terminate_timeout_fd); + term->shutdown.terminate_timeout_fd = -1; + } + + if (term->shutdown.in_progress) + shutdown_maybe_done(term); + else if (!term->conf->hold_at_exit) + term_shutdown(term); +} + +static bool +fdm_shutdown(struct fdm *fdm, int fd, int events, void *data) +{ + struct terminal *term = data; + + /* Kill the event FD */ + fdm_del(term->fdm, fd); + + struct wl_window *win = term->window; + + if (win != NULL && win->tab_count <= 1) { + /* + * Last (or only) tab — destroy the whole window. + * + * Normally we'd get unmapped when we destroy the Wayland + * surface above. However, it appears that under certain + * conditions, those events are deferred (for example, when + * a screen locker is active), and thus we can get here + * without having been unmapped. + */ + wayl_win_destroy(win); + term->window = NULL; + + struct wayland *wayl = term->wl; + tll_foreach(wayl->seats, it) { + if (it->item.kbd_focus == term) + it->item.kbd_focus = NULL; + if (it->item.mouse_focus == term) + it->item.mouse_focus = NULL; + } + } + /* + * Multi-tab case: leave term->window intact so term_destroy() can + * use its existing tab-removal and tab-switch logic. + */ + + term->shutdown.fdm_done = true; + shutdown_maybe_done(term); + return true; +} + +static bool +fdm_terminate_timeout(struct fdm *fdm, int fd, int events, void *data) +{ + uint64_t unused; + ssize_t bytes = read(fd, &unused, sizeof(unused)); + if (bytes < 0) { + LOG_ERRNO("failed to read from slave terminate timeout FD"); + return false; + } + + struct terminal *term = data; + xassert(!term->shutdown.client_has_terminated); + + LOG_DBG("slave (PID=%u) has not terminated, sending %s (%d)", + term->slave, + term->shutdown.next_signal == SIGTERM ? "SIGTERM" + : term->shutdown.next_signal == SIGKILL ? "SIGKILL" + : "", + term->shutdown.next_signal); + + kill(-term->slave, term->shutdown.next_signal); + + switch (term->shutdown.next_signal) { + case SIGTERM: + term->shutdown.next_signal = SIGKILL; + break; + + case SIGKILL: + /* Disarm. Shouldn't be necessary, as we should be able to + shutdown completely after sending SIGKILL, before the next + timeout occurs). But lets play it safe... */ + if (term->shutdown.terminate_timeout_fd >= 0) { + timerfd_settime( + term->shutdown.terminate_timeout_fd, 0, + &(const struct itimerspec){0}, NULL); + } + break; + + default: + BUG("can only handle SIGTERM and SIGKILL"); + return false; + } + + return true; +} + +bool +term_shutdown(struct terminal *term) +{ + if (term->shutdown.in_progress) + return true; + + term->shutdown.in_progress = true; + + /* + * Close FDs then postpone self-destruction to the next poll + * iteration, by creating an event FD that we trigger immediately. + */ + + term_cursor_blink_update(term); + xassert(term->cursor_blink.fd < 0); + + fdm_del(term->fdm, term->selection.auto_scroll.fd); + fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); + fdm_del(term->fdm, term->render.app_id.timer_fd); + fdm_del(term->fdm, term->render.icon.timer_fd); + fdm_del(term->fdm, term->render.title.timer_fd); + fdm_del(term->fdm, term->delayed_render_timer.lower_fd); + fdm_del(term->fdm, term->delayed_render_timer.upper_fd); + fdm_del(term->fdm, term->blink.fd); + fdm_del(term->fdm, term->flash.fd); + + del_utmp_record(term->conf, term->reaper, term->ptmx); + + if (term->window != NULL && term->window->is_configured) + fdm_del(term->fdm, term->ptmx); + else + close(term->ptmx); + + if (!term->shutdown.client_has_terminated) { + if (term->slave <= 0) { + term->shutdown.client_has_terminated = true; + } else { + LOG_DBG("initiating asynchronous terminate of slave; " + "sending SIGHUP to PID=%u", term->slave); + + kill(-term->slave, SIGHUP); + + /* + * Set up a timer, with an interval - on the first timeout + * we'll send SIGTERM. If the the client application still + * isn't terminating, we'll wait an additional interval, + * and then send SIGKILL. + */ + const struct itimerspec timeout = {.it_value = {.tv_sec = 30}, + .it_interval = {.tv_sec = 30}}; + + int timeout_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (timeout_fd < 0 || + timerfd_settime(timeout_fd, 0, &timeout, NULL) < 0 || + !fdm_add(term->fdm, timeout_fd, EPOLLIN, &fdm_terminate_timeout, term)) + { + if (timeout_fd >= 0) + close(timeout_fd); + LOG_ERRNO("failed to create slave terminate timeout FD"); + return false; + } + + xassert(term->shutdown.terminate_timeout_fd < 0); + term->shutdown.terminate_timeout_fd = timeout_fd; + term->shutdown.next_signal = SIGTERM; + } + } + + term->selection.auto_scroll.fd = -1; + term->render.app_sync_updates.timer_fd = -1; + term->render.app_id.timer_fd = -1; + term->render.icon.timer_fd = -1; + term->render.title.timer_fd = -1; + term->delayed_render_timer.lower_fd = -1; + term->delayed_render_timer.upper_fd = -1; + term->blink.fd = -1; + term->flash.fd = -1; + term->ptmx = -1; + + int event_fd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK); + if (event_fd == -1) { + LOG_ERRNO("failed to create terminal shutdown event FD"); + return false; + } + + if (!fdm_add(term->fdm, event_fd, EPOLLIN, &fdm_shutdown, term)) { + close(event_fd); + return false; + } + + if (write(event_fd, &(uint64_t){1}, sizeof(uint64_t)) != sizeof(uint64_t)) { + LOG_ERRNO("failed to send terminal shutdown event"); + fdm_del(term->fdm, event_fd); + return false; + } + + return true; +} + +static volatile sig_atomic_t alarm_raised; + +static void +sig_alarm(int signo) +{ + LOG_DBG("SIGALRM"); + alarm_raised = 1; +} + +int +term_destroy(struct terminal *term) +{ + if (term == NULL) + return 0; + + tll_foreach(term->wl->terms, it) { + if (it->item == term) { + tll_remove(term->wl->terms, it); + break; + } + } + + del_utmp_record(term->conf, term->reaper, term->ptmx); + + fdm_del(term->fdm, term->selection.auto_scroll.fd); + fdm_del(term->fdm, term->render.app_sync_updates.timer_fd); + fdm_del(term->fdm, term->render.app_id.timer_fd); + fdm_del(term->fdm, term->render.icon.timer_fd); + fdm_del(term->fdm, term->render.title.timer_fd); + fdm_del(term->fdm, term->delayed_render_timer.lower_fd); + fdm_del(term->fdm, term->delayed_render_timer.upper_fd); + fdm_del(term->fdm, term->cursor_blink.fd); + fdm_del(term->fdm, term->blink.fd); + fdm_del(term->fdm, term->flash.fd); + fdm_del(term->fdm, term->ptmx); + if (term->shutdown.terminate_timeout_fd >= 0) + fdm_del(term->fdm, term->shutdown.terminate_timeout_fd); + + if (term->window != NULL) { + struct wl_window *win = term->window; + + /* Remove ourselves from the window's tab list */ + bool was_active = (win->term == term); + size_t our_idx = win->tab_count; /* invalid sentinel */ + for (size_t i = 0; i < win->tab_count; i++) { + if (win->tabs[i] == term) { + our_idx = i; + memmove(&win->tabs[i], &win->tabs[i + 1], + (win->tab_count - i - 1) * sizeof(win->tabs[0])); + win->tab_count--; + if (win->active_tab > 0 && win->active_tab >= win->tab_count) + win->active_tab = win->tab_count - 1; + else if (our_idx < win->active_tab) + win->active_tab--; + break; + } + } + + if (win->tab_count == 0) { + /* Last tab - destroy the window normally */ + wayl_win_destroy(win); + } else { + /* Other tabs remain - just purge our render chains */ + + /* Cancel any pending frame callback that still holds a pointer + * to this terminal, preventing a use-after-free when the + * compositor fires it after term is freed. */ + if (win->frame_callback != NULL) { + wl_callback_destroy(win->frame_callback); + win->frame_callback = NULL; + } + + render_wait_for_preapply_damage(term); + shm_purge(term->render.chains.search); + shm_purge(term->render.chains.scrollback_indicator); + shm_purge(term->render.chains.render_timer); + shm_purge(term->render.chains.grid); + shm_purge(term->render.chains.url); + shm_purge(term->render.chains.csd); + shm_purge(term->render.chains.tab_bar); + shm_purge(term->render.chains.tab_overview); + shm_purge(term->render.chains.overlay); + + /* Switch to the new active tab if needed */ + if (was_active) { + struct terminal *next = win->tabs[win->active_tab]; + win->term = next; + next->active_surface = term->active_surface; + + /* Update keyboard and mouse focus */ + bool has_kbd_focus = false; + tll_foreach(next->wl->seats, it) { + struct seat *seat = &it->item; + if (seat->kbd_focus == term) + seat->kbd_focus = next; + if (seat->mouse_focus == term) + seat->mouse_focus = next; + if (seat->kbd_focus == next) + has_kbd_focus = true; + } + + if (has_kbd_focus) { + term_kbd_focus_in(next); + term_visual_focus_in(next); + } else { + term_visual_focus_out(next); + } + + render_resize(next, + (int)roundf(term->width / term->scale), + (int)roundf(term->height / term->scale), + RESIZE_FORCE); + render_refresh_csd(next); + render_refresh_tab_bar(next); + term_xcursor_update(next); + } else { + /* Just update the tab bar to reflect removed tab */ + render_refresh_tab_bar(win->term); + } + } + + term->window = NULL; + } + + mtx_lock(&term->render.workers.lock); + xassert(tll_length(term->render.workers.queue) == 0); + + /* Count livinig threads - we may get here when only some of the + * threads have been successfully started */ + size_t worker_count = 0; + if (term->render.workers.threads != NULL) { + for (size_t i = 0; i < term->render.workers.count; i++, worker_count++) { + if (term->render.workers.threads[i] == 0) + break; + } + + for (size_t i = 0; i < worker_count; i++) { + sem_post(&term->render.workers.start); + tll_push_back(term->render.workers.queue, -2); + } + } + mtx_unlock(&term->render.workers.lock); + + key_binding_unref(term->wl->key_binding_manager, term->conf); + + urls_reset(term); + + free(term->vt.osc.data); + free(term->vt.osc8.uri); + + composed_free(term->composed); + + free(term->app_id); + free(term->window_title); + tll_free_and_free(term->window_title_stack, free); + + for (size_t i = 0; i < sizeof(term->fonts) / sizeof(term->fonts[0]); i++) + fcft_destroy(term->fonts[i]); + for (size_t i = 0; i < 4; i++) + free(term->font_sizes[i]); + + + free_custom_glyphs( + &term->custom_glyphs.box_drawing, GLYPH_BOX_DRAWING_COUNT); + free_custom_glyphs( + &term->custom_glyphs.braille, GLYPH_BRAILLE_COUNT); + free_custom_glyphs( + &term->custom_glyphs.legacy, GLYPH_LEGACY_COUNT); + free_custom_glyphs( + &term->custom_glyphs.octants, GLYPH_OCTANTS_COUNT); + + free(term->search.buf); + free(term->search.last.buf); + + /* Free search history */ + { + struct search_history_entry *e = term->search.history_head; + while (e != NULL) { + struct search_history_entry *next = e->next; + free(e->buf); + free(e); + e = next; + } + } + + /* Free compiled regex if any */ + if (term->search.regex_compiled != NULL) { + regfree(term->search.regex_compiled); + free(term->search.regex_compiled); + } + + if (term->render.workers.threads != NULL) { + for (size_t i = 0; i < term->render.workers.count; i++) { + if (term->render.workers.threads[i] != 0) + thrd_join(term->render.workers.threads[i], NULL); + } + } + free(term->render.workers.threads); + mtx_destroy(&term->render.workers.preapplied_damage.lock); + cnd_destroy(&term->render.workers.preapplied_damage.cond); + mtx_destroy(&term->render.workers.lock); + sem_destroy(&term->render.workers.start); + sem_destroy(&term->render.workers.done); + xassert(tll_length(term->render.workers.queue) == 0); + tll_free(term->render.workers.queue); + + shm_unref(term->render.last_buf); + shm_chain_free(term->render.chains.grid); + shm_chain_free(term->render.chains.search); + shm_chain_free(term->render.chains.scrollback_indicator); + shm_chain_free(term->render.chains.render_timer); + shm_chain_free(term->render.chains.url); + shm_chain_free(term->render.chains.csd); + shm_chain_free(term->render.chains.overlay); + shm_chain_free(term->render.chains.tab_bar); + shm_chain_free(term->render.chains.tab_overview); + pixman_region32_fini(&term->render.last_overlay_clip); + + tll_free(term->tab_stops); + + tll_foreach(term->ptmx_buffers, it) { + free(it->item.data); + tll_remove(term->ptmx_buffers, it); + } + tll_foreach(term->ptmx_paste_buffers, it) { + free(it->item.data); + tll_remove(term->ptmx_paste_buffers, it); + } + + notify_free(term, &term->kitty_notification); + tll_foreach(term->active_notifications, it) { + notify_free(term, &it->item); + tll_remove(term->active_notifications, it); + } + + for (size_t i = 0; i < ALEN(term->notification_icons); i++) + notify_icon_free(&term->notification_icons[i]); + + sixel_fini(term); + + term_ime_reset(term); + + grid_free(&term->normal); + grid_free(&term->alt); + grid_free(term->interactive_resizing.grid); + free(term->interactive_resizing.grid); + + free(term->foot_exe); + free(term->cwd); + free(term->mouse_user_cursor); + free(term->color_stack.stack); + + int ret = EXIT_SUCCESS; + + if (term->slave > 0) { + /* We'll deal with this explicitly */ + reaper_del(term->reaper, term->slave); + + int exit_status; + + if (term->shutdown.client_has_terminated) + exit_status = term->shutdown.exit_status; + else { + LOG_DBG("initiating blocking terminate of slave; " + "sending SIGHUP to PID=%u", term->slave); + + kill(-term->slave, SIGHUP); + + /* + * we've closed the ptxm, and sent SIGTERM to the client + * application. It *should* exit... + * + * But, since it is possible to write clients that ignore + * this, we need to handle it in *some* way. + * + * So, what we do is register a SIGALRM handler, and configure a 30 + * second alarm. If the slave hasn't died after this time, we send + * it a SIGKILL, + * + * Note that this solution is *not* asynchronous, and any + * other events etc will be ignored during this time. This of + * course only applies to a 'foot --server' instance, where + * there might be other terminals running. + */ + struct sigaction action = {.sa_handler = &sig_alarm}; + sigemptyset(&action.sa_mask); + sigaction(SIGALRM, &action, NULL); + + /* Wait, then send SIGTERM, wait again, then send SIGKILL */ + int next_signal = SIGTERM; + + alarm_raised = 0; + alarm(30); + + while (true) { + int r = waitpid(term->slave, &exit_status, 0); + + if (r == term->slave) + break; + + if (r == -1) { + xassert(errno == EINTR); + + if (alarm_raised) { + LOG_DBG("slave (PID=%u) has not terminated yet, " + "sending: %s (%d)", term->slave, + next_signal == SIGTERM ? "SIGTERM" : "SIGKILL", + next_signal); + + kill(-term->slave, next_signal); + next_signal = SIGKILL; + + alarm_raised = 0; + alarm(30); + } + } + } + + /* Cancel alarm */ + alarm(0); + action.sa_handler = SIG_DFL; + sigaction(SIGALRM, &action, NULL); + } + + ret = EXIT_FAILURE; + if (WIFEXITED(exit_status)) { + ret = WEXITSTATUS(exit_status); + LOG_DBG("slave exited with code %d", ret); + } else if (WIFSIGNALED(exit_status)) { + ret = WTERMSIG(exit_status); + LOG_WARN("slave exited with signal %d (%s)", ret, strsignal(ret)); + } else { + LOG_WARN("slave exited for unknown reason (status = 0x%08x)", + exit_status); + } + } + + free(term); + +#if defined(__GLIBC__) + if (!malloc_trim(0)) + LOG_WARN("failed to trim memory"); +#endif + + return ret; +} + +static inline void +erase_cell_range(struct terminal *term, struct row *row, int start, int end) +{ + xassert(start < term->cols); + xassert(end < term->cols); + + row->dirty = true; + + const enum color_source bg_src = term->vt.attrs.bg_src; + + if (unlikely(bg_src != COLOR_DEFAULT)) { + for (int col = start; col <= end; col++) { + struct cell *c = &row->cells[col]; + c->wc = 0; + c->attrs = (struct attributes){.bg_src = bg_src, .bg = term->vt.attrs.bg}; + } + } else + memset(&row->cells[start], 0, (end - start + 1) * sizeof(row->cells[0])); + + if (unlikely(row->extra != NULL)) { + grid_row_uri_range_erase(row, start, end); + grid_row_underline_range_erase(row, start, end); + } +} + +static inline void +erase_line(struct terminal *term, struct row *row) +{ + erase_cell_range(term, row, 0, term->cols - 1); + row->linebreak = true; + row->shell_integration.prompt_marker = false; + row->shell_integration.cmd_start = -1; + row->shell_integration.cmd_end = -1; +} + +static void +term_theme_apply(struct terminal *term, const struct color_theme *theme) +{ + term->colors.fg = theme->fg; + term->colors.bg = theme->bg; + term->colors.alpha = theme->alpha; + term->colors.cursor_fg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.text; + term->colors.cursor_bg = (theme->use_custom.cursor ? 1u << 31 : 0) | theme->cursor.cursor; + term->colors.selection_fg = theme->selection_fg; + term->colors.selection_bg = theme->selection_bg; + memcpy(term->colors.table, theme->table, sizeof(term->colors.table)); +} + +void +term_reset(struct terminal *term, bool hard) +{ + LOG_INFO("%s resetting the terminal", hard ? "hard" : "soft"); + + term->cursor_keys_mode = CURSOR_KEYS_NORMAL; + term->keypad_keys_mode = KEYPAD_NUMERICAL; + term->reverse = false; + term->hide_cursor = false; + term->reverse_wrap = true; + term->auto_margin = true; + term->insert_mode = false; + term->bracketed_paste = false; + term->focus_events = false; + term->num_lock_modifier = true; + term->bell_action_enabled = true; + term->mouse_tracking = MOUSE_NONE; + term->mouse_reporting = MOUSE_NORMAL; + term->charsets.selected = G0; + term->charsets.set[G0] = CHARSET_ASCII; + term->charsets.set[G1] = CHARSET_ASCII; + term->charsets.set[G2] = CHARSET_ASCII; + term->charsets.set[G3] = CHARSET_ASCII; + term->saved_charsets = term->charsets; + tll_free_and_free(term->window_title_stack, free); + term_set_window_title(term, term->conf->title); + term_set_app_id(term, NULL); + + term_set_user_mouse_cursor(term, NULL); + + term->modify_other_keys_2 = false; + memset(term->normal.kitty_kbd.flags, 0, sizeof(term->normal.kitty_kbd.flags)); + memset(term->alt.kitty_kbd.flags, 0, sizeof(term->alt.kitty_kbd.flags)); + term->normal.kitty_kbd.idx = term->alt.kitty_kbd.idx = 0; + + term->scroll_region.start = 0; + term->scroll_region.end = term->rows; + + free(term->vt.osc8.uri); + free(term->vt.osc.data); + + term->vt = (struct vt){ + .state = 0, /* STATE_GROUND */ + }; + + if (term->grid == &term->alt) { + term->grid = &term->normal; + selection_cancel(term); + } + + term->meta.esc_prefix = true; + term->meta.eight_bit = true; + + tll_foreach(term->normal.sixel_images, it) { + sixel_destroy(&it->item); + tll_remove(term->normal.sixel_images, it); + } + tll_foreach(term->alt.sixel_images, it) { + sixel_destroy(&it->item); + tll_remove(term->alt.sixel_images, it); + } + + notify_free(term, &term->kitty_notification); + tll_foreach(term->active_notifications, it) { + notify_free(term, &it->item); + tll_remove(term->active_notifications, it); + } + + for (size_t i = 0; i < ALEN(term->notification_icons); i++) + notify_icon_free(&term->notification_icons[i]); + + term->grapheme_shaping = term->conf->tweak.grapheme_shaping; + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + term_ime_enable(term); +#endif + + term->bits_affecting_ascii_printer.value = 0; + term_update_ascii_printer(term); + + if (!hard) + return; + + const struct color_theme *theme = NULL; + + switch (term->conf->initial_color_theme) { + case COLOR_THEME_DARK: theme = &term->conf->colors_dark; break; + case COLOR_THEME_LIGHT: theme = &term->conf->colors_light; break; + case COLOR_THEME_1: BUG("COLOR_THEME_1 should not be used"); break; + case COLOR_THEME_2: BUG("COLOR_THEME_2 should not be used"); break; + } + + term->flash.active = false; + term->blink.state = BLINK_ON; + fdm_del(term->fdm, term->blink.fd); term->blink.fd = -1; + term_theme_apply(term, theme); + term->colors.active_theme = term->conf->initial_color_theme; + free(term->color_stack.stack); + term->color_stack.stack = NULL; + term->color_stack.size = 0; + term->color_stack.idx = 0; + term->origin = ORIGIN_ABSOLUTE; + term->normal.cursor.lcf = false; + term->alt.cursor.lcf = false; + term->normal.cursor = (struct cursor){.point = {0, 0}}; + term->normal.saved_cursor = (struct cursor){.point = {0, 0}}; + term->alt.cursor = (struct cursor){.point = {0, 0}}; + term->alt.saved_cursor = (struct cursor){.point = {0, 0}}; + term->cursor_style = term->conf->cursor.style; + term->cursor_blink.decset = false; + term->cursor_blink.deccsusr = term->conf->cursor.blink.enabled; + term_cursor_blink_update(term); + selection_cancel(term); + term->normal.offset = term->normal.view = 0; + term->alt.offset = term->alt.view = 0; + for (size_t i = 0; i < term->rows; i++) { + struct row *r = grid_row_and_alloc(&term->normal, i); + erase_line(term, r); + } + for (size_t i = 0; i < term->rows; i++) { + struct row *r = grid_row_and_alloc(&term->alt, i); + erase_line(term, r); + } + for (size_t i = term->rows; i < term->normal.num_rows; i++) { + grid_row_free(term->normal.rows[i]); + term->normal.rows[i] = NULL; + } + for (size_t i = term->rows; i < term->alt.num_rows; i++) { + grid_row_free(term->alt.rows[i]); + term->alt.rows[i] = NULL; + } + term->normal.cur_row = term->normal.rows[0]; + term->alt.cur_row = term->alt.rows[0]; + tll_free(term->normal.scroll_damage); + tll_free(term->alt.scroll_damage); + term->render.last_cursor.row = NULL; + term_damage_all(term); + + term->sixel.scrolling = true; + term->sixel.cursor_right_of_graphics = false; + term->sixel.use_private_palette = true; + term->sixel.max_width = SIXEL_MAX_WIDTH; + term->sixel.max_height = SIXEL_MAX_HEIGHT; + term->sixel.palette_size = SIXEL_MAX_COLORS; + free(term->sixel.private_palette); + free(term->sixel.shared_palette); + term->sixel.private_palette = term->sixel.shared_palette = NULL; +} + +static bool +term_font_size_adjust_by_points(struct terminal *term, float amount) +{ + const struct config *conf = term->conf; + const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + + for (size_t j = 0; j < font_list->count; j++) { + struct config_font *font = &term->font_sizes[i][j]; + float old_pt_size = font->pt_size; + + if (font->px_size > 0) + old_pt_size = font->px_size * 72. / dpi; + + font->pt_size = fmaxf(old_pt_size + amount, 0.); + font->px_size = -1; + } + } + + return reload_fonts(term, true); +} + +static bool +term_font_size_adjust_by_pixels(struct terminal *term, int amount) +{ + const struct config *conf = term->conf; + const float dpi = term->font_is_sized_by_dpi ? term->font_dpi : 96.; + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + + for (size_t j = 0; j < font_list->count; j++) { + struct config_font *font = &term->font_sizes[i][j]; + int old_px_size = font->px_size; + + if (font->px_size <= 0) + old_px_size = font->pt_size * dpi / 72.; + + font->px_size = max(old_px_size + amount, 1); + } + } + + return reload_fonts(term, true); +} + +static bool +term_font_size_adjust_by_percent(struct terminal *term, bool increment, float percent) +{ + const struct config *conf = term->conf; + const float multiplier = increment + ? 1. + percent + : 1. / (1. + percent); + + for (size_t i = 0; i < 4; i++) { + const struct config_font_list *font_list = &conf->fonts[i]; + + for (size_t j = 0; j < font_list->count; j++) { + struct config_font *font = &term->font_sizes[i][j]; + + if (font->px_size > 0) + font->px_size = max(font->px_size * multiplier, 1); + else + font->pt_size = fmax(font->pt_size * multiplier, 0); + } + } + + return reload_fonts(term, true); +} + +bool +term_font_size_increase(struct terminal *term) +{ + const struct config *conf = term->conf; + const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment; + + if (inc_dec->percent > 0.) + return term_font_size_adjust_by_percent(term, true, inc_dec->percent); + else if (inc_dec->pt_or_px.px > 0) + return term_font_size_adjust_by_pixels(term, inc_dec->pt_or_px.px); + else + return term_font_size_adjust_by_points(term, inc_dec->pt_or_px.pt); +} + +bool +term_font_size_decrease(struct terminal *term) +{ + const struct config *conf = term->conf; + const struct font_size_adjustment *inc_dec = &conf->font_size_adjustment; + + if (inc_dec->percent > 0.) + return term_font_size_adjust_by_percent(term, false, inc_dec->percent); + else if (inc_dec->pt_or_px.px > 0) + return term_font_size_adjust_by_pixels(term, -inc_dec->pt_or_px.px); + else + return term_font_size_adjust_by_points(term, -inc_dec->pt_or_px.pt); +} + +bool +term_font_size_reset(struct terminal *term) +{ + return load_fonts_from_conf(term); +} + +bool +term_fractional_scaling(const struct terminal *term) +{ + return term->wl->fractional_scale_manager != NULL && + term->wl->viewporter != NULL && + term->window->scale > 0.; +} + +bool +term_preferred_buffer_scale(const struct terminal *term) +{ + return term->window->preferred_buffer_scale > 0; +} + +bool +term_update_scale(struct terminal *term) +{ + const struct wl_window *win = term->window; + + /* + * We have a number of "sources" we can use as scale. We choose + * the scale in the following order: + * + * - "preferred" scale, from the fractional-scale-v1 protocol + * - "preferred" scale, from wl_compositor version 6. + NOTE: if the compositor advertises version 6 we must use 1.0 + until wl_surface.preferred_buffer_scale is sent + * - scaling factor of output we most recently were mapped on + * - if we're not mapped, use the last known scaling factor + * - if we're not mapped, and we don't have a last known scaling + * factor, use the scaling factor from the first available + * output. + * - if there aren't any outputs available, use 1.0 + */ + const float new_scale = (term_fractional_scaling(term) + ? win->scale + : term_preferred_buffer_scale(term) + ? win->preferred_buffer_scale + : tll_length(win->on_outputs) > 0 + ? tll_back(win->on_outputs)->scale + : term->scale_before_unmap > 0. + ? term->scale_before_unmap + : tll_length(term->wl->monitors) > 0 + ? tll_front(term->wl->monitors).scale + : 1.); + + if (new_scale == term->scale) + return false; + + LOG_DBG("scaling factor changed: %.2f -> %.2f", term->scale, new_scale); + term->scale_before_unmap = new_scale; + term->scale = new_scale; + return true; +} + +bool +term_font_dpi_changed(struct terminal *term, float old_scale) +{ + float dpi = get_font_dpi(term); + xassert(term->scale > 0.); + + bool was_scaled_using_dpi = term->font_is_sized_by_dpi; + bool will_scale_using_dpi = term->conf->dpi_aware; + + bool need_font_reload = + was_scaled_using_dpi != will_scale_using_dpi || + (will_scale_using_dpi + ? term->font_dpi != dpi + : old_scale != term->scale); + + if (need_font_reload) { + LOG_DBG("DPI/scale change: DPI-aware=%s, " + "DPI: %.2f -> %.2f, scale: %.2f -> %.2f, " + "sizing font based on monitor's %s", + term->conf->dpi_aware ? "yes" : "no", + term->font_dpi, dpi, old_scale, term->scale, + will_scale_using_dpi ? "DPI" : "scaling factor"); + } + + term->font_dpi = dpi; + term->font_dpi_before_unmap = dpi; + term->font_is_sized_by_dpi = will_scale_using_dpi; + + if (!need_font_reload) + return false; + + return reload_fonts(term, false); +} + +void +term_font_subpixel_changed(struct terminal *term) +{ + enum fcft_subpixel subpixel = get_font_subpixel(term); + + if (term->font_subpixel == subpixel) + return; + +#if defined(_DEBUG) && LOG_ENABLE_DBG + static const char *const str[] = { + [FCFT_SUBPIXEL_DEFAULT] = "default", + [FCFT_SUBPIXEL_NONE] = "disabled", + [FCFT_SUBPIXEL_HORIZONTAL_RGB] = "RGB", + [FCFT_SUBPIXEL_HORIZONTAL_BGR] = "BGR", + [FCFT_SUBPIXEL_VERTICAL_RGB] = "V-RGB", + [FCFT_SUBPIXEL_VERTICAL_BGR] = "V-BGR", + }; + + LOG_DBG("subpixel mode changed: %s -> %s", str[term->font_subpixel], str[subpixel]); +#endif + + term->font_subpixel = subpixel; + term_damage_view(term); + render_refresh(term); +} + +int +term_font_baseline(const struct terminal *term) +{ + const struct fcft_font *font = term->fonts[0]; + const int line_height = term->cell_height; + const int font_height = font->ascent + font->descent; + + /* + * Center glyph on the line *if* using a custom line height, + * otherwise the baseline is simply 'descent' pixels above the + * bottom of the cell + */ + const int glyph_top_y = term->font_line_height.px >= 0 + ? round((line_height - font_height) / 2.) + : 0; + + return term->font_y_ofs + line_height - glyph_top_y - font->descent; +} + +void +term_damage_rows(struct terminal *term, int start, int end) +{ + xassert(start <= end); + for (int r = start; r <= end; r++) { + struct row *row = grid_row(term->grid, r); + row->dirty = true; + for (int c = 0; c < term->grid->num_cols; c++) + row->cells[c].attrs.clean = 0; + } +} + +void +term_damage_rows_in_view(struct terminal *term, int start, int end) +{ + xassert(start <= end); + for (int r = start; r <= end; r++) { + struct row *row = grid_row_in_view(term->grid, r); + row->dirty = true; + for (int c = 0; c < term->grid->num_cols; c++) + row->cells[c].attrs.clean = 0; + } +} + +void +term_damage_all(struct terminal *term) +{ + term_damage_rows(term, 0, term->rows - 1); +} + +void +term_damage_view(struct terminal *term) +{ + term_damage_rows_in_view(term, 0, term->rows - 1); +} + +void +term_damage_cursor(struct terminal *term) +{ + term->grid->cur_row->cells[term->grid->cursor.point.col].attrs.clean = 0; + term->grid->cur_row->dirty = true; +} + +void +term_damage_margins(struct terminal *term) +{ + term->render.margins = true; +} + +void +term_damage_color(struct terminal *term, enum color_source src, int idx) +{ + xassert(src == COLOR_DEFAULT || src == COLOR_BASE256); + + for (int r = 0; r < term->rows; r++) { + struct row *row = grid_row_in_view(term->grid, r); + struct cell *cell = &row->cells[0]; + const struct cell *end = &row->cells[term->cols]; + + for (; cell < end; cell++) { + bool dirty = false; + + switch (cell->attrs.fg_src) { + case COLOR_BASE16: + case COLOR_BASE256: + if (src == COLOR_BASE256 && cell->attrs.fg == idx) + dirty = true; + break; + + case COLOR_DEFAULT: + if (src == COLOR_DEFAULT) { + /* Doesn't matter whether we've updated the + default foreground, or background, we still + want to dirty this cell, to be sure we handle + all cases of color inversion/reversal */ + dirty = true; + } + break; + + case COLOR_RGB: + /* Not affected */ + break; + } + + switch (cell->attrs.bg_src) { + case COLOR_BASE16: + case COLOR_BASE256: + if (src == COLOR_BASE256 && cell->attrs.bg == idx) + dirty = true; + break; + + case COLOR_DEFAULT: + if (src == COLOR_DEFAULT) { + /* Doesn't matter whether we've updated the + default foreground, or background, we still + want to dirty this cell, to be sure we handle + all cases of color inversion/reversal */ + dirty = true; + } + break; + + case COLOR_RGB: + /* Not affected */ + break; + } + + if (dirty) { + cell->attrs.clean = 0; + row->dirty = true; + } + } + + /* Colored underlines */ + if (row->extra != NULL) { + const struct row_ranges *underlines = &row->extra->underline_ranges; + + for (int i = 0; i < underlines->count; i++) { + const struct row_range *range = &underlines->v[i]; + + /* Underline colors are either default, or + BASE256/RGB, but never BASE16 */ + xassert(range->underline.color_src == COLOR_DEFAULT || + range->underline.color_src == COLOR_BASE256 || + range->underline.color_src == COLOR_RGB); + + if (range->underline.color_src == src) { + struct cell *c = &row->cells[range->start]; + const struct cell *e = &row->cells[range->end + 1]; + + for (; c < e; c++) + c->attrs.clean = 0; + + row->dirty = true; + } + } + } + } +} + +void +term_damage_scroll(struct terminal *term, enum damage_type damage_type, + struct scroll_region region, int lines) +{ + if (likely(tll_length(term->grid->scroll_damage) > 0)) { + struct damage *dmg = &tll_back(term->grid->scroll_damage); + + if (likely( + dmg->type == damage_type && + dmg->region.start == region.start && + dmg->region.end == region.end)) + { + /* Make sure we don't overflow... */ + int new_line_count = (int)dmg->lines + lines; + if (likely(new_line_count <= UINT16_MAX)) { + dmg->lines = new_line_count; + return; + } + } + } + struct damage dmg = { + .type = damage_type, + .region = region, + .lines = lines, + }; + tll_push_back(term->grid->scroll_damage, dmg); +} + +void +term_erase(struct terminal *term, int start_row, int start_col, + int end_row, int end_col) +{ + xassert(start_row <= end_row); + xassert(start_col <= end_col || start_row < end_row); + + if (start_row == end_row) { + struct row *row = grid_row(term->grid, start_row); + erase_cell_range(term, row, start_col, end_col); + sixel_overwrite_by_row(term, start_row, start_col, end_col - start_col + 1); + return; + } + + xassert(end_row > start_row); + + erase_cell_range( + term, grid_row(term->grid, start_row), start_col, term->cols - 1); + sixel_overwrite_by_row(term, start_row, start_col, term->cols - start_col); + + for (int r = start_row + 1; r < end_row; r++) + erase_line(term, grid_row(term->grid, r)); + sixel_overwrite_by_rectangle( + term, start_row + 1, 0, end_row - start_row, term->cols); + + erase_cell_range(term, grid_row(term->grid, end_row), 0, end_col); + sixel_overwrite_by_row(term, end_row, 0, end_col + 1); +} + +void +term_erase_scrollback(struct terminal *term) +{ + const struct grid *grid = term->grid; + const int num_rows = grid->num_rows; + const int mask = num_rows - 1; + + const int scrollback_history_size = num_rows - term->rows; + if (scrollback_history_size == 0) + return; + + const int start = (grid->offset + term->rows) & mask; + const int end = (grid->offset - 1) & mask; + + const int rel_start = grid_row_abs_to_sb(grid, term->rows, start); + const int rel_end = grid_row_abs_to_sb(grid, term->rows, end); + + const int sel_start = selection_get_start(term).row; + const int sel_end = selection_get_end(term).row; + + if (sel_end >= 0) { + /* + * Cancel selection if it touches any of the rows in the + * scrollback, since we can't have the selection reference + * soon-to-be deleted rows. + * + * This is done by range checking the selection range against + * the scrollback range. + * + * To make this comparison simpler, the start/end absolute row + * numbers are "rebased" against the scrollback start, where + * row 0 is the *first* row in the scrollback. A high number + * thus means the row is further *down* in the scrollback, + * closer to the screen bottom. + */ + + const int rel_sel_start = grid_row_abs_to_sb(grid, term->rows, sel_start); + const int rel_sel_end = grid_row_abs_to_sb(grid, term->rows, sel_end); + + if ((rel_sel_start <= rel_start && rel_sel_end >= rel_start) || + (rel_sel_start <= rel_end && rel_sel_end >= rel_end) || + (rel_sel_start >= rel_start && rel_sel_end <= rel_end)) + { + selection_cancel(term); + } + } + + tll_foreach(term->grid->sixel_images, it) { + struct sixel *six = &it->item; + const int six_start = grid_row_abs_to_sb(grid, term->rows, six->pos.row); + const int six_end = grid_row_abs_to_sb( + grid, term->rows, six->pos.row + six->rows - 1); + + if ((six_start <= rel_start && six_end >= rel_start) || + (six_start <= rel_end && six_end >= rel_end) || + (six_start >= rel_start && six_end <= rel_end)) + { + sixel_destroy(six); + tll_remove(term->grid->sixel_images, it); + } + } + + for (int i = start;; i = (i + 1) & mask) { + struct row *row = term->grid->rows[i]; + if (row != NULL) { + if (term->render.last_cursor.row == row) + term->render.last_cursor.row = NULL; + + grid_row_free(row); + term->grid->rows[i] = NULL; + } + + if (i == end) + break; + } + + term->grid->view = term->grid->offset; + +#if defined(_DEBUG) + for (int i = 0; i < term->rows; i++) { + xassert(grid_row_in_view(term->grid, i) != NULL); + } +#endif + + term_damage_view(term); +} + +UNITTEST +{ + const int scrollback_rows = 16; + const int term_rows = 5; + const int cols = 5; + + struct fdm *fdm = fdm_init(); + xassert(fdm != NULL); + + struct terminal term = { + .fdm = fdm, + .rows = term_rows, + .cols = cols, + .normal = { + .rows = xcalloc(scrollback_rows, sizeof(term.normal.rows[0])), + .num_rows = scrollback_rows, + .num_cols = cols, + }, + .grid = &term.normal, + .selection = { + .coords = { + .start = {-1, -1}, + .end = {-1, -1}, + }, + .kind = SELECTION_NONE, + .auto_scroll = { + .fd = -1, + }, + }, + }; + +#define populate_scrollback() do { \ + for (int i = 0; i < scrollback_rows; i++) { \ + if (term.normal.rows[i] == NULL) { \ + struct row *r = xcalloc(1, sizeof(*term.normal.rows[i])); \ + r->cells = xcalloc(cols, sizeof(r->cells[0])); \ + term.normal.rows[i] = r; \ + } \ + } \ + } while (0) + + /* + * Test case 1 - no selection, just verify all rows except those + * on screen have been deleted. + */ + + populate_scrollback(); + term.normal.offset = 11; + term_erase_scrollback(&term); + for (int i = 0; i < scrollback_rows; i++) { + if (i >= term.normal.offset && i < term.normal.offset + term_rows) + xassert(term.normal.rows[i] != NULL); + else + xassert(term.normal.rows[i] == NULL); + } + + /* + * Test case 2 - selection that touches the scrollback. Verify the + * selection is cancelled. + */ + + term.normal.offset = 14; /* Screen covers rows 14,15,0,1,2 */ + + /* Selection covers rows 15,0,1,2,3 */ + term.selection.coords.start = (struct coord){.row = 15}; + term.selection.coords.end = (struct coord){.row = 19}; + term.selection.kind = SELECTION_CHAR_WISE; + + populate_scrollback(); + term_erase_scrollback(&term); + xassert(term.selection.coords.start.row < 0); + xassert(term.selection.coords.end.row < 0); + xassert(term.selection.kind == SELECTION_NONE); + + /* + * Test case 3 - selection that does *not* touch the + * scrollback. Verify the selection is *not* cancelled. + */ + + /* Selection covers rows 15,0 */ + term.selection.coords.start = (struct coord){.row = 15}; + term.selection.coords.end = (struct coord){.row = 16}; + term.selection.kind = SELECTION_CHAR_WISE; + + populate_scrollback(); + term_erase_scrollback(&term); + xassert(term.selection.coords.start.row == 15); + xassert(term.selection.coords.end.row == 16); + xassert(term.selection.kind == SELECTION_CHAR_WISE); + + term.selection.coords.start = (struct coord){-1, -1}; + term.selection.coords.end = (struct coord){-1, -1}; + term.selection.kind = SELECTION_NONE; + + /* + * Test case 4 - sixel that touch the scrollback + */ + + struct sixel six = { + .rows = 5, + .pos = { + .row = 15, + }, + }; + tll_push_back(term.normal.sixel_images, six); + populate_scrollback(); + term_erase_scrollback(&term); + xassert(tll_length(term.normal.sixel_images) == 0); + + /* + * Test case 5 - sixel that does *not* touch the scrollback + */ + six.rows = 3; + tll_push_back(term.normal.sixel_images, six); + populate_scrollback(); + term_erase_scrollback(&term); + xassert(tll_length(term.normal.sixel_images) == 1); + + /* Cleanup */ + tll_free(term.normal.sixel_images); + xassert(term.selection.auto_scroll.fd == -1); + for (int i = 0; i < scrollback_rows; i++) + grid_row_free(term.normal.rows[i]); + free(term.normal.rows); + fdm_destroy(fdm); +} + +int +term_row_rel_to_abs(const struct terminal *term, int row) +{ + switch (term->origin) { + case ORIGIN_ABSOLUTE: + return min(row, term->rows - 1); + + case ORIGIN_RELATIVE: + return min(row + term->scroll_region.start, term->scroll_region.end - 1); + } + + BUG("Invalid cursor_origin value"); + return -1; +} + +void +term_cursor_to(struct terminal *term, int row, int col) +{ + xassert(row < term->rows); + xassert(col < term->cols); + + term->grid->cursor.lcf = false; + + term->grid->cursor.point.col = col; + term->grid->cursor.point.row = row; + + term_reset_grapheme_state(term); + + term->grid->cur_row = grid_row(term->grid, row); +} + +void +term_cursor_home(struct terminal *term) +{ + term_cursor_to(term, term_row_rel_to_abs(term, 0), 0); +} + +void +term_cursor_col(struct terminal *term, int col) +{ + xassert(col < term->cols); + + term->grid->cursor.lcf = false; + term->grid->cursor.point.col = col; + term_reset_grapheme_state(term); +} + +void +term_cursor_left(struct terminal *term, int count) +{ + int move_amount = min(term->grid->cursor.point.col, count); + term->grid->cursor.point.col -= move_amount; + xassert(term->grid->cursor.point.col >= 0); + term->grid->cursor.lcf = false; + term_reset_grapheme_state(term); +} + +void +term_cursor_right(struct terminal *term, int count) +{ + int move_amount = min(term->cols - term->grid->cursor.point.col - 1, count); + term->grid->cursor.point.col += move_amount; + xassert(term->grid->cursor.point.col < term->cols); + term->grid->cursor.lcf = false; + term_reset_grapheme_state(term); +} + +void +term_cursor_up(struct terminal *term, int count) +{ + int top = term->origin == ORIGIN_ABSOLUTE ? 0 : term->scroll_region.start; + xassert(term->grid->cursor.point.row >= top); + + int move_amount = min(term->grid->cursor.point.row - top, count); + term_cursor_to(term, term->grid->cursor.point.row - move_amount, term->grid->cursor.point.col); +} + +void +term_cursor_down(struct terminal *term, int count) +{ + int bottom = term->origin == ORIGIN_ABSOLUTE ? term->rows : term->scroll_region.end; + xassert(bottom >= term->grid->cursor.point.row); + + int move_amount = min(bottom - term->grid->cursor.point.row - 1, count); + term_cursor_to(term, term->grid->cursor.point.row + move_amount, term->grid->cursor.point.col); +} + +static bool +cursor_blink_rearm_timer(struct terminal *term) +{ + if (term->cursor_blink.fd < 0) { + int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (fd < 0) { + LOG_ERRNO("failed to create cursor blink timer FD"); + return false; + } + + if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_cursor_blink, term)) { + close(fd); + return false; + } + + term->cursor_blink.fd = fd; + } + + const int rate_ms = term->conf->cursor.blink.rate_ms; + const long secs = rate_ms / 1000; + const long nsecs = (rate_ms % 1000) * 1000000; + + const struct itimerspec timer = { + .it_value = {.tv_sec = secs, .tv_nsec = nsecs}, + .it_interval = {.tv_sec = secs, .tv_nsec = nsecs}, + }; + + if (timerfd_settime(term->cursor_blink.fd, 0, &timer, NULL) < 0) { + LOG_ERRNO("failed to arm cursor blink timer"); + fdm_del(term->fdm, term->cursor_blink.fd); + term->cursor_blink.fd = -1; + return false; + } + + return true; +} + +static bool +cursor_blink_disarm_timer(struct terminal *term) +{ + fdm_del(term->fdm, term->cursor_blink.fd); + term->cursor_blink.fd = -1; + return true; +} + +void +term_cursor_blink_update(struct terminal *term) +{ + bool enable = term->cursor_blink.decset || term->cursor_blink.deccsusr; + bool activate = !term->shutdown.in_progress && enable && term->visual_focus; + + LOG_DBG("decset=%d, deccsrusr=%d, focus=%d, shutting-down=%d, enable=%d, activate=%d", + term->cursor_blink.decset, term->cursor_blink.deccsusr, + term->visual_focus, term->shutdown.in_progress, + enable, activate); + + if (activate && term->cursor_blink.fd < 0) { + term->cursor_blink.state = CURSOR_BLINK_ON; + cursor_blink_rearm_timer(term); + } else if (!activate && term->cursor_blink.fd >= 0) + cursor_blink_disarm_timer(term); +} + +static bool +selection_on_top_region(const struct terminal *term, + struct scroll_region region) +{ + return region.start > 0 && + selection_on_rows(term, 0, region.start - 1); +} + +static bool +selection_on_bottom_region(const struct terminal *term, + struct scroll_region region) +{ + return region.end < term->rows && + selection_on_rows(term, region.end, term->rows - 1); +} + +void +term_scroll_partial(struct terminal *term, struct scroll_region region, int rows) +{ + LOG_DBG("scroll: rows=%d, region.start=%d, region.end=%d", + rows, region.start, region.end); + + /* Verify scroll amount has been clamped */ + xassert(rows <= region.end - region.start); + + /* Cancel selections that cannot be scrolled */ + if (unlikely(term->selection.coords.end.row >= 0)) { + /* + * Selection is (partly) inside either the top or bottom + * scrolling regions, or on (at least one) of the lines + * scrolled in (i.e. reused lines). + */ + if (selection_on_top_region(term, region) || + selection_on_bottom_region(term, region)) + { + selection_cancel(term); + } else + selection_scroll_up(term, rows); + } + + sixel_scroll_up(term, rows); + + /* How many lines from the scrollback start is the current viewport? */ + int view_sb_start_distance = grid_row_abs_to_sb( + term->grid, term->rows, term->grid->view); + + bool view_follows = term->grid->view == term->grid->offset; + term->grid->offset += rows; + term->grid->offset &= term->grid->num_rows - 1; + + if (likely(view_follows)) { + term_damage_scroll(term, DAMAGE_SCROLL, region, rows); + selection_view_down(term, term->grid->offset); + term->grid->view = term->grid->offset; + } else if (unlikely(rows > view_sb_start_distance)) { + /* Part of current view is being scrolled out */ + int new_view = grid_row_sb_to_abs(term->grid, term->rows, 0); + selection_view_down(term, new_view); + cmd_scrollback_down(term, rows - view_sb_start_distance); + } + + /* Top non-scrolling region. */ + for (int i = region.start - 1; i >= 0; i--) + grid_swap_row(term->grid, i - rows, i); + + /* Bottom non-scrolling region */ + for (int i = term->rows - 1; i >= region.end; i--) + grid_swap_row(term->grid, i - rows, i); + + /* Erase scrolled in lines */ + for (int r = region.end - rows; r < region.end; r++) { + struct row *row = grid_row_and_alloc(term->grid, r); + erase_line(term, row); + } + + term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); + +#if defined(_DEBUG) + for (int r = 0; r < term->rows; r++) + xassert(grid_row(term->grid, r) != NULL); +#endif +} + +void +term_scroll(struct terminal *term, int rows) +{ + term_scroll_partial(term, term->scroll_region, rows); +} + +void +term_scroll_reverse_partial(struct terminal *term, + struct scroll_region region, int rows) +{ + LOG_DBG("scroll reverse: rows=%d, region.start=%d, region.end=%d", + rows, region.start, region.end); + + /* Verify scroll amount has been clamped */ + xassert(rows <= region.end - region.start); + + /* Cancel selections that cannot be scrolled */ + if (unlikely(term->selection.coords.end.row >= 0)) { + /* + * Selection is (partly) inside either the top or bottom + * scrolling regions, or on (at least one) of the lines + * scrolled in (i.e. reused lines). + */ + if (selection_on_top_region(term, region) || + selection_on_bottom_region(term, region)) + { + selection_cancel(term); + } else + selection_scroll_down(term, rows); + } + + /* Unallocate scrolled out lines */ + for (int r = region.end - rows; r < region.end; r++) { + const int abs_r = grid_row_absolute(term->grid, r); + struct row *row = term->grid->rows[abs_r]; + + grid_row_free(row); + term->grid->rows[abs_r] = NULL; + + if (term->render.last_cursor.row == row) + term->render.last_cursor.row = NULL; + } + + sixel_scroll_down(term, rows); + + const bool view_follows = term->grid->view == term->grid->offset; + term->grid->offset -= rows; + term->grid->offset += term->grid->num_rows; + term->grid->offset &= term->grid->num_rows - 1; + + /* How many lines from the scrollback start is the current viewport? */ + const int view_sb_start_distance = grid_row_abs_to_sb( + term->grid, term->rows, term->grid->view); + const int offset_sb_start_distance = grid_row_abs_to_sb( + term->grid, term->rows, term->grid->offset); + + xassert(term->grid->offset >= 0); + xassert(term->grid->offset < term->grid->num_rows); + + if (view_follows) { + term_damage_scroll(term, DAMAGE_SCROLL_REVERSE, region, rows); + selection_view_up(term, term->grid->offset); + term->grid->view = term->grid->offset; + } else if (unlikely(view_sb_start_distance > offset_sb_start_distance)) { + /* Part of current view is being scrolled out */ + int new_view = term->grid->offset; + selection_view_up(term, new_view); + term->grid->view = new_view; + } + + /* Bottom non-scrolling region */ + for (int i = region.end + rows; i < term->rows + rows; i++) + grid_swap_row(term->grid, i, i - rows); + + /* Top non-scrolling region */ + for (int i = 0 + rows; i < region.start + rows; i++) + grid_swap_row(term->grid, i, i - rows); + + /* Erase scrolled in lines */ + for (int r = region.start; r < region.start + rows; r++) { + struct row *row = grid_row_and_alloc(term->grid, r); + erase_line(term, row); + } + + if (unlikely(view_sb_start_distance > offset_sb_start_distance)) + term_damage_view(term); + + term->grid->cur_row = grid_row(term->grid, term->grid->cursor.point.row); + +#if defined(_DEBUG) + for (int r = 0; r < term->rows; r++) + xassert(grid_row(term->grid, r) != NULL); + for (int r = 0; r < term->rows; r++) + xassert(grid_row_in_view(term->grid, r) != NULL); +#endif +} + +void +term_scroll_reverse(struct terminal *term, int rows) +{ + term_scroll_reverse_partial(term, term->scroll_region, rows); +} + +void +term_carriage_return(struct terminal *term) +{ + term_cursor_left(term, term->grid->cursor.point.col); +} + +void +term_linefeed(struct terminal *term) +{ + term->grid->cursor.lcf = false; + + if (term->grid->cursor.point.row == term->scroll_region.end - 1) + term_scroll(term, 1); + else + term_cursor_down(term, 1); + + term_reset_grapheme_state(term); +} + +void +term_reverse_index(struct terminal *term) +{ + if (term->grid->cursor.point.row == term->scroll_region.start) + term_scroll_reverse(term, 1); + else + term_cursor_up(term, 1); +} + +void +term_reset_view(struct terminal *term) +{ + if (term->grid->view == term->grid->offset) + return; + + term->grid->view = term->grid->offset; + term_damage_view(term); +} + +void +term_save_cursor(struct terminal *term) +{ + term->grid->saved_cursor = term->grid->cursor; + term->vt.saved_attrs = term->vt.attrs; + term->saved_charsets = term->charsets; +} + +void +term_restore_cursor(struct terminal *term, const struct cursor *cursor) +{ + int row = min(cursor->point.row, term->rows - 1); + int col = min(cursor->point.col, term->cols - 1); + + term_cursor_to(term, row, col); + term->grid->cursor.lcf = cursor->lcf; + + term->vt.attrs = term->vt.saved_attrs; + term->charsets = term->saved_charsets; + + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); +} + +void +term_visual_focus_in(struct terminal *term) +{ + if (term->visual_focus) + return; + + term->visual_focus = true; + term_cursor_blink_update(term); + render_refresh_csd(term); +} + +void +term_visual_focus_out(struct terminal *term) +{ + if (!term->visual_focus) + return; + + term->visual_focus = false; + term_cursor_blink_update(term); + render_refresh_csd(term); +} + +void +term_kbd_focus_in(struct terminal *term) +{ + if (term->kbd_focus) + return; + + term->kbd_focus = true; + + if (term->render.urgency) { + term->render.urgency = false; + term_damage_margins(term); + } + + cursor_refresh(term); + + if (term->focus_events) + term_to_slave(term, "\033[I", 3); +} + +void +term_kbd_focus_out(struct terminal *term) +{ + if (!term->kbd_focus) + return; + + tll_foreach(term->wl->seats, it) + if (it->item.kbd_focus == term) + return; + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (term_ime_reset(term)) + render_refresh(term); +#endif + + term->kbd_focus = false; + cursor_refresh(term); + + if (term->focus_events) + term_to_slave(term, "\033[O", 3); +} + +static int +linux_mouse_button_to_x(int button) +{ + /* Note: on X11, scroll events where reported as buttons. Not so + * on Wayland. We manually map scroll events to custom "button" + * defines (BTN_WHEEL_*). + */ + switch (button) { + case BTN_LEFT: return 1; + case BTN_MIDDLE: return 2; + case BTN_RIGHT: return 3; + case BTN_WHEEL_BACK: return 4; /* Foot custom define */ + case BTN_WHEEL_FORWARD: return 5; /* Foot custom define */ + case BTN_WHEEL_LEFT: return 6; /* Foot custom define */ + case BTN_WHEEL_RIGHT: return 7; /* Foot custom define */ + case BTN_SIDE: return 8; + case BTN_EXTRA: return 9; + case BTN_FORWARD: return 10; + case BTN_BACK: return 11; + case BTN_TASK: return 12; /* Guessing... */ + + default: + LOG_WARN("unrecognized mouse button: %d (0x%x)", button, button); + return -1; + } +} + +static int +encode_xbutton(int xbutton) +{ + switch (xbutton) { + case 1: case 2: case 3: + return xbutton - 1; + + case 4: case 5: case 6: case 7: + /* Like button 1 and 2, but with 64 added */ + return xbutton - 4 + 64; + + case 8: case 9: case 10: case 11: + /* Similar to 4 and 5, but adding 128 instead of 64 */ + return xbutton - 8 + 128; + + default: + LOG_ERR("cannot encode X mouse button: %d", xbutton); + return -1; + } +} + +static void +report_mouse_click(struct terminal *term, int encoded_button, int row, int col, + int row_pixels, int col_pixels, bool release) +{ + char response[128]; + + switch (term->mouse_reporting) { + case MOUSE_NORMAL: { + int encoded_col = 32 + col + 1; + int encoded_row = 32 + row + 1; + if (encoded_col > 255 || encoded_row > 255) + return; + + snprintf(response, sizeof(response), "\033[M%c%c%c", + 32 + (release ? 3 : encoded_button), encoded_col, encoded_row); + break; + } + + case MOUSE_SGR: + snprintf(response, sizeof(response), "\033[<%d;%d;%d%c", + encoded_button, col + 1, row + 1, release ? 'm' : 'M'); + break; + + case MOUSE_SGR_PIXELS: { + const int bounded_col = max(col_pixels, 0); + const int bounded_row = max(row_pixels, 0); + snprintf(response, sizeof(response), "\033[<%d;%d;%d%c", + encoded_button, bounded_col + 1, bounded_row + 1, release ? 'm' : 'M'); + break; + } + + case MOUSE_URXVT: + snprintf(response, sizeof(response), "\033[%d;%d;%dM", + 32 + (release ? 3 : encoded_button), col + 1, row + 1); + break; + + case MOUSE_UTF8: + /* Unimplemented */ + return; + } + + term_to_slave(term, response, strlen(response)); +} + +static void +report_mouse_motion(struct terminal *term, int encoded_button, int row, int col, int row_pixels, int col_pixels) +{ + report_mouse_click(term, encoded_button, row, col, row_pixels, col_pixels, false); +} + +bool +term_mouse_grabbed(const struct terminal *term, const struct seat *seat) +{ + /* + * Mouse is grabbed by us, regardless of whether mouse tracking + * has been enabled or not. + */ + + xkb_mod_mask_t mods; + get_current_modifiers(seat, &mods, NULL, 0, true); + + const struct key_binding_set *bindings = + key_binding_for(term->wl->key_binding_manager, term->conf, seat); + const xkb_mod_mask_t override_modmask = bindings->selection_overrides; + bool override_mods_pressed = (mods & override_modmask) == override_modmask; + + return term->mouse_tracking == MOUSE_NONE || + (seat->kbd_focus == term && override_mods_pressed); +} + +void +term_mouse_down(struct terminal *term, int button, int row, int col, + int row_pixels, int col_pixels, + bool _shift, bool _alt, bool _ctrl) +{ + /* Map libevent button event code to X button number */ + int xbutton = linux_mouse_button_to_x(button); + if (xbutton == -1) + return; + + int encoded = encode_xbutton(xbutton); + if (encoded == -1) + return; + + + bool has_focus = term->kbd_focus; + bool shift = has_focus ? _shift : false; + bool alt = has_focus ? _alt : false; + bool ctrl = has_focus ? _ctrl : false; + + encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0); + + switch (term->mouse_tracking) { + case MOUSE_NONE: + break; + + case MOUSE_CLICK: + case MOUSE_DRAG: + case MOUSE_MOTION: + report_mouse_click(term, encoded, row, col, row_pixels, col_pixels, false); + break; + + case MOUSE_X10: + /* Never enabled */ + BUG("X10 mouse mode not implemented"); + break; + } +} + +void +term_mouse_up(struct terminal *term, int button, int row, int col, + int row_pixels, int col_pixels, + bool _shift, bool _alt, bool _ctrl) +{ + /* Map libevent button event code to X button number */ + int xbutton = linux_mouse_button_to_x(button); + if (xbutton == -1) + return; + + if (xbutton == 4 || xbutton == 5) { + /* No release events for vertical scroll wheel buttons */ + return; + } + + int encoded = encode_xbutton(xbutton); + if (encoded == -1) + return; + + bool has_focus = term->kbd_focus; + bool shift = has_focus ? _shift : false; + bool alt = has_focus ? _alt : false; + bool ctrl = has_focus ? _ctrl : false; + + encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0); + + switch (term->mouse_tracking) { + case MOUSE_NONE: + break; + + case MOUSE_CLICK: + case MOUSE_DRAG: + case MOUSE_MOTION: + report_mouse_click(term, encoded, row, col, row_pixels, col_pixels, true); + break; + + case MOUSE_X10: + /* Never enabled */ + BUG("X10 mouse mode not implemented"); + break; + } +} + +void +term_mouse_motion(struct terminal *term, int button, int row, int col, + int row_pixels, int col_pixels, + bool _shift, bool _alt, bool _ctrl) +{ + int encoded = 0; + + if (button != 0) { + /* Map libevent button event code to X button number */ + int xbutton = linux_mouse_button_to_x(button); + if (xbutton == -1) + return; + + encoded = encode_xbutton(xbutton); + if (encoded == -1) + return; + } else + encoded = 3; /* "released" */ + + bool has_focus = term->kbd_focus; + bool shift = has_focus ? _shift : false; + bool alt = has_focus ? _alt : false; + bool ctrl = has_focus ? _ctrl : false; + + encoded += 32; /* Motion event */ + encoded += (shift ? 4 : 0) + (alt ? 8 : 0) + (ctrl ? 16 : 0); + + switch (term->mouse_tracking) { + case MOUSE_NONE: + case MOUSE_CLICK: + return; + + case MOUSE_DRAG: + if (button == 0) + return; + /* FALLTHROUGH */ + + case MOUSE_MOTION: + report_mouse_motion(term, encoded, row, col, row_pixels, col_pixels); + break; + + case MOUSE_X10: + /* Never enabled */ + BUG("X10 mouse mode not implemented"); + break; + } +} + +void +term_xcursor_update_for_seat(struct terminal *term, struct seat *seat) +{ + enum cursor_shape shape = CURSOR_SHAPE_NONE; + + switch (term->active_surface) { + case TERM_SURF_GRID: + if (seat->pointer.hidden) + shape = CURSOR_SHAPE_HIDDEN; + + else if (cursor_string_to_server_shape( + term->mouse_user_cursor, + term->wl->shape_manager_version) != 0 || + render_xcursor_is_valid(seat, term->mouse_user_cursor)) + { + shape = CURSOR_SHAPE_CUSTOM; + } + + else if (term_mouse_grabbed(term, seat)) { + shape = CURSOR_SHAPE_TEXT; + } + + else + shape = CURSOR_SHAPE_LEFT_PTR; + break; + + case TERM_SURF_TITLE: + case TERM_SURF_BUTTON_MINIMIZE: + case TERM_SURF_BUTTON_MAXIMIZE: + case TERM_SURF_BUTTON_CLOSE: + shape = CURSOR_SHAPE_LEFT_PTR; + break; + + case TERM_SURF_TAB_BAR: { + size_t idx = term_tab_bar_hit_test(term, seat->mouse.x, seat->mouse.y); + shape = (idx != SIZE_MAX) ? CURSOR_SHAPE_POINTER : CURSOR_SHAPE_LEFT_PTR; + break; + } + + case TERM_SURF_TAB_OVERVIEW: + shape = CURSOR_SHAPE_POINTER; + break; + + case TERM_SURF_BORDER_LEFT: + case TERM_SURF_BORDER_RIGHT: + case TERM_SURF_BORDER_TOP: + case TERM_SURF_BORDER_BOTTOM: + shape = xcursor_for_csd_border(term, seat->mouse.x, seat->mouse.y); + break; + + case TERM_SURF_NONE: + return; + } + + if (shape == CURSOR_SHAPE_NONE) + BUG("xcursor not set"); + + render_xcursor_set(seat, term, shape); +} + +void +term_xcursor_update(struct terminal *term) +{ + tll_foreach(term->wl->seats, it) + term_xcursor_update_for_seat(term, &it->item); +} + +void +term_set_window_title(struct terminal *term, const char *title) +{ + if (term->conf->locked_title && term->window_title_has_been_set) + return; + + if (term->window_title != NULL && streq(term->window_title, title)) + return; + + if (!is_valid_utf8_and_printable(title)) { + /* It's an xdg_toplevel::set_title() protocol violation to set + a title with an invalid UTF-8 sequence */ + LOG_WARN("%s: title is not valid UTF-8, ignoring", title); + return; + } + + free(term->window_title); + term->window_title = xstrdup(title); + render_refresh_title(term); + render_refresh_tab_bar(term); + term->window_title_has_been_set = true; +} + +void +term_set_app_id(struct terminal *term, const char *app_id) +{ + if (app_id != NULL && *app_id == '\0') + app_id = NULL; + + if (term->app_id == NULL && app_id == NULL) + return; + + if (term->app_id != NULL && app_id != NULL && streq(term->app_id, app_id)) + return; + + if (app_id != NULL && !is_valid_utf8_and_printable(app_id)) { + LOG_WARN("%s: app-id is not valid UTF-8, ignoring", app_id); + return; + } + + free(term->app_id); + if (app_id != NULL) { + term->app_id = xstrdup(app_id); + } else { + term->app_id = NULL; + } + + const size_t length = app_id != NULL ? strlen(app_id) : 0; + if (length > 2048) { + /* + * Not sure if there's a limit in the protocol, or the + * libwayland implementation, or e.g. wlroots, but too long + * app-id's (not e.g. title) causes at least river and sway to + * peg the CPU at 100%, and stop sending e.g. frame callbacks. + * + */ + term->app_id[2048] = '\0'; + } + + render_refresh_app_id(term); + render_refresh_icon(term); +} + +const char * +term_icon(const struct terminal *term) +{ + const char *app_id = + term->app_id != NULL ? term->app_id : term->conf->app_id; + + return +#if 0 +term->window_icon != NULL + ? term->window_icon + : + #endif + streq(app_id, "footclient") + ? "foot" + : app_id; +} + +void +term_flash(struct terminal *term, unsigned duration_ms) +{ + LOG_DBG("FLASH for %ums", duration_ms); + + struct itimerspec alarm = { + .it_value = {.tv_sec = 0, .tv_nsec = duration_ms * 1000000}, + }; + + if (timerfd_settime(term->flash.fd, 0, &alarm, NULL) < 0) + LOG_ERRNO("failed to arm flash timer"); + else { + term->flash.active = true; + } +} + +void +term_bell(struct terminal *term) +{ + + if (!term->bell_action_enabled) + return; + + if (term->conf->bell.urgent && !term->kbd_focus) { + if (!wayl_win_set_urgent(term->window)) { + /* + * Urgency (xdg-activation) is relatively new in + * Wayland. Fallback to our old, "faked", urgency - + * rendering our window margins in red + */ + term->render.urgency = true; + term_damage_margins(term); + } + } + + if (term->conf->bell.system_bell) + wayl_win_ring_bell(term->window); + + if (term->conf->bell.notify) { + notify_notify(term, &(struct notification){ + .title = xstrdup("Bell"), + .body = xstrdup("Bell in terminal"), + .expire_time = -1, + .focus = true, + }); + } + + if (term->conf->bell.flash) + term_flash(term, 100); + + if ((term->conf->bell.command.argv.args != NULL) && + (!term->kbd_focus || term->conf->bell.command_focused)) + { + int devnull = open("/dev/null", O_RDONLY); + spawn(term->reaper, NULL, term->conf->bell.command.argv.args, + devnull, -1, -1, NULL, NULL, NULL); + + if (devnull >= 0) + close(devnull); + } +} + +bool +term_spawn_new(const struct terminal *term) +{ + char *argv[4]; + int argc = 0; + + argv[argc++] = term->foot_exe; + if (term->conf->conf_path != NULL) { + argv[argc++] = "--config"; + argv[argc++] = term->conf->conf_path; + } + argv[argc] = NULL; + + return spawn( + term->reaper, term->cwd, argv, + -1, -1, -1, NULL, NULL, NULL) >= 0; +} + +void +term_enable_app_sync_updates(struct terminal *term) +{ + term->render.app_sync_updates.enabled = true; + + if (timerfd_settime( + term->render.app_sync_updates.timer_fd, 0, + &(struct itimerspec){.it_value = {.tv_sec = 1}}, NULL) < 0) + { + LOG_ERR("failed to arm timer for application synchronized updates"); + } + + /* Disable pending refresh *iff* the grid is the *only* thing + * scheduled to be re-rendered */ + if (!term->render.refresh.csd && !term->render.refresh.search && + !term->render.pending.csd && !term->render.pending.search) + { + term->render.refresh.grid = false; + term->render.pending.grid = false; + } + + /* Disarm delayed rendering timers */ + timerfd_settime( + term->delayed_render_timer.lower_fd, 0, + &(struct itimerspec){{0}}, NULL); + timerfd_settime( + term->delayed_render_timer.upper_fd, 0, + &(struct itimerspec){{0}}, NULL); + term->delayed_render_timer.is_armed = false; +} + +void +term_disable_app_sync_updates(struct terminal *term) +{ + if (!term->render.app_sync_updates.enabled) + return; + + term->render.app_sync_updates.enabled = false; + render_refresh(term); + + /* Reset timers */ + timerfd_settime( + term->render.app_sync_updates.timer_fd, 0, + &(struct itimerspec){{0}}, NULL); +} + +static inline void +print_linewrap(struct terminal *term) +{ + if (likely(!term->grid->cursor.lcf)) { + /* Not and end of line */ + return; + } + + if (unlikely(!term->auto_margin)) { + /* Auto-wrap disabled */ + return; + } + + term->grid->cur_row->linebreak = false; + term->grid->cursor.lcf = false; + + const int row = term->grid->cursor.point.row; + + if (row == term->scroll_region.end - 1) + term_scroll(term, 1); + else { + const int new_row = min(row + 1, term->rows - 1); + term->grid->cursor.point.row = new_row; + term->grid->cur_row = grid_row(term->grid, new_row); + } + + term->grid->cursor.point.col = 0; +} + +static inline void +print_insert(struct terminal *term, int width) +{ + if (likely(!term->insert_mode)) + return; + + xassert(width > 0); + + struct row *row = term->grid->cur_row; + const size_t move_count = max(0, term->cols - term->grid->cursor.point.col - width); + + memmove( + &row->cells[term->grid->cursor.point.col + width], + &row->cells[term->grid->cursor.point.col], + move_count * sizeof(struct cell)); + + /* Mark moved cells as dirty */ + for (size_t i = term->grid->cursor.point.col + width; i < term->cols; i++) + row->cells[i].attrs.clean = 0; +} + +static void +print_spacer(struct terminal *term, int col, int remaining) +{ + struct grid *grid = term->grid; + struct row *row = grid->cur_row; + struct cell *cell = &row->cells[col]; + + cell->wc = CELL_SPACER + remaining; + cell->attrs = (struct attributes){0}; +} + +/* + * Puts a character on the grid. Coordinates are in screen coordinates + * (i.e. ‘cursor’ coordinates). + * + * Does NOT: + * - update the cursor + * - linewrap + * - erase sixels + * + * Limitations: + * - double width characters not supported + */ +void +term_fill(struct terminal *term, int r, int c, uint8_t data, size_t count, + bool use_sgr_attrs) +{ + struct row *row = grid_row(term->grid, r); + row->dirty = true; + + xassert(c + count <= term->cols); + + struct attributes attrs = use_sgr_attrs + ? term->vt.attrs + : (struct attributes){0}; + + const struct cell *last = &row->cells[c + count]; + for (struct cell *cell = &row->cells[c]; cell < last; cell++) { + cell->wc = data; + cell->attrs = attrs; + + /* TODO: why do we print the URI here, and then erase it below? */ + if (unlikely(use_sgr_attrs && term->vt.osc8.uri != NULL)) { + grid_row_uri_range_put(row, c, term->vt.osc8.uri, term->vt.osc8.id); + + switch (term->conf->url.osc8_underline) { + case OSC8_UNDERLINE_ALWAYS: + cell->attrs.url = true; + break; + + case OSC8_UNDERLINE_URL_MODE: + break; + } + } + + if (unlikely(use_sgr_attrs && + (term->vt.underline.style > UNDERLINE_SINGLE || + term->vt.underline.color_src != COLOR_DEFAULT))) + { + grid_row_underline_range_put(row, c, term->vt.underline); + } + } + + if (unlikely(row->extra != NULL)) { + if (likely(term->vt.osc8.uri != NULL)) + grid_row_uri_range_erase(row, c, c + count - 1); + + if (likely(term->vt.underline.style <= UNDERLINE_SINGLE && + term->vt.underline.color_src == COLOR_DEFAULT)) + { + /* No extended/styled underlines active, so erase any such + attributes at the target columns */ + grid_row_underline_range_erase(row, c, c + count - 1); + } + } +} + +void +term_print(struct terminal *term, char32_t wc, int width, bool insert_mode_disable) +{ + xassert(width > 0); + + struct grid *grid = term->grid; + + if (unlikely(term->charsets.set[term->charsets.selected] == CHARSET_GRAPHIC) && + wc >= 0x60 && wc <= 0x7e) + { + /* 0x60 - 0x7e */ + static const char32_t vt100_0[] = { + U'◆', U'▒', U'␉', U'␌', U'␍', U'␊', U'°', U'±', /* ` - g */ + U'␤', U'␋', U'┘', U'┐', U'┌', U'└', U'┼', U'⎺', /* h - o */ + U'⎻', U'─', U'⎼', U'⎽', U'├', U'┤', U'┴', U'┬', /* p - w */ + U'│', U'≤', U'≥', U'π', U'≠', U'£', U'·', /* x - ~ */ + }; + + xassert(width == 1); + wc = vt100_0[wc - 0x60]; + } + + print_linewrap(term); + if (!insert_mode_disable) + print_insert(term, width); + + int col = grid->cursor.point.col; + + if (unlikely(width > 1) && likely(term->auto_margin) && + col + width > term->cols) + { + /* Multi-column character that doesn't fit on current line - + * pad with spacers */ + for (size_t i = col; i < term->cols; i++) + print_spacer(term, i, 0); + + /* And force a line-wrap */ + grid->cursor.lcf = 1; + print_linewrap(term); + col = 0; + } + + sixel_overwrite_at_cursor(term, width); + + /* *Must* get current cell *after* linewrap+insert */ + struct row *row = grid->cur_row; + row->dirty = true; + row->linebreak = true; + + struct cell *cell = &row->cells[col]; + cell->wc = term->vt.last_printed = wc; + cell->attrs = term->vt.attrs; + + if (unlikely(term->vt.osc8.uri != NULL)) { + for (int i = 0; i < width && (col + i) < term->cols; i++) { + grid_row_uri_range_put( + row, col + i, term->vt.osc8.uri, term->vt.osc8.id); + } + + switch (term->conf->url.osc8_underline) { + case OSC8_UNDERLINE_ALWAYS: + cell->attrs.url = true; + break; + + case OSC8_UNDERLINE_URL_MODE: + break; + } + } else if (row->extra != NULL) + grid_row_uri_range_erase(row, col, col + width - 1); + + if (unlikely(term->vt.underline.style > UNDERLINE_SINGLE || + term->vt.underline.color_src != COLOR_DEFAULT)) + { + grid_row_underline_range_put(row, col, term->vt.underline); + } else if (row->extra != NULL) + grid_row_underline_range_erase(row, col, col + width - 1); + + /* Advance cursor the 'additional' columns while dirty:ing the cells */ + for (int i = 1; i < width && (col + 1) < term->cols; i++) { + col++; + print_spacer(term, col, width - i); + } + + xassert(col < term->cols); + + /* Advance cursor */ + if (unlikely(++col >= term->cols)) { + grid->cursor.lcf = true; + col--; + } else + xassert(!grid->cursor.lcf); + + grid->cursor.point.col = col; +} + +static void +ascii_printer_generic(struct terminal *term, char32_t wc) +{ + term_print(term, wc, 1, false); +} + +static void +ascii_printer_fast(struct terminal *term, char32_t wc) +{ + struct grid *grid = term->grid; + + xassert(term->charsets.set[term->charsets.selected] == CHARSET_ASCII); + xassert(!term->insert_mode); + xassert(tll_length(grid->sixel_images) == 0); + + print_linewrap(term); + + /* *Must* get current cell *after* linewrap+insert */ + int col = grid->cursor.point.col; + const int uri_start = col; + + struct row *row = grid->cur_row; + row->dirty = true; + row->linebreak = true; + + struct cell *cell = &row->cells[col]; + cell->wc = term->vt.last_printed = wc; + cell->attrs = term->vt.attrs; + + /* Advance cursor */ + if (unlikely(++col >= term->cols)) { + xassert(col == term->cols); + grid->cursor.lcf = true; + col--; + } else + xassert(!grid->cursor.lcf); + + grid->cursor.point.col = col; + + if (unlikely(row->extra != NULL)) { + grid_row_uri_range_erase(row, uri_start, uri_start); + grid_row_underline_range_erase(row, uri_start, uri_start); + } +} + +static void +ascii_printer_single_shift(struct terminal *term, char32_t wc) +{ + ascii_printer_generic(term, wc); + term->charsets.selected = term->charsets.saved; + + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); +} + +void +term_update_ascii_printer(struct terminal *term) +{ + _Static_assert(sizeof(term->bits_affecting_ascii_printer) == sizeof(uint8_t), "bad size"); + + void (*new_printer)(struct terminal *term, char32_t wc) = + unlikely(term->bits_affecting_ascii_printer.value != 0) + ? &ascii_printer_generic + : &ascii_printer_fast; + +#if defined(_DEBUG) && LOG_ENABLE_DBG + if (term->ascii_printer != new_printer) { + LOG_DBG("switching ASCII printer %s -> %s", + term->ascii_printer == &ascii_printer_fast ? "fast" : "generic", + new_printer == &ascii_printer_fast ? "fast" : "generic"); + } +#endif + + term->ascii_printer = new_printer; +} + +void +term_single_shift(struct terminal *term, enum charset_designator idx) +{ + term->charsets.saved = term->charsets.selected; + term->charsets.selected = idx; + term->ascii_printer = &ascii_printer_single_shift; +} + +#if defined(FOOT_GRAPHEME_CLUSTERING) +static int +emoji_vs_compare(const void *_key, const void *_entry) +{ + const struct emoji_vs *key = _key; + const struct emoji_vs *entry = _entry; + + uint32_t cp = key->start; + + if (cp < entry->start) + return -1; + else if (cp > entry->end) + return 1; + else + return 0; +} + +UNITTEST +{ + /* Verify the emoji_vs list is sorted */ + int64_t last_end = -1; + + for (size_t i = 0; i < sizeof(emoji_vs) / sizeof(emoji_vs[0]); i++) { + const struct emoji_vs *vs = &emoji_vs[i]; + xassert(vs->start <= vs->end); + xassert(vs->start > last_end); + xassert(vs->vs15 || vs->vs16); + last_end = vs->end; + } +} +#endif + +void +term_process_and_print_non_ascii(struct terminal *term, char32_t wc) +{ + int width = c32width(wc); + bool insert_mode_disable = false; + const bool grapheme_clustering = term->grapheme_shaping; + +#if !defined(FOOT_GRAPHEME_CLUSTERING) + xassert(!grapheme_clustering); +#endif + + if (term->grid->cursor.point.col > 0 && + (grapheme_clustering || + (!grapheme_clustering && width == 0 && wc >= 0x300))) + { + int col = term->grid->cursor.point.col; + if (!term->grid->cursor.lcf) + col--; + + /* Skip past spacers */ + struct row *row = term->grid->cur_row; + while (row->cells[col].wc >= CELL_SPACER && col > 0) + col--; + + xassert(col >= 0 && col < term->cols); + char32_t base = row->cells[col].wc; + char32_t UNUSED last = base; + + /* Is base cell already a cluster? */ + const struct composed *composed = + (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) + ? composed_lookup(term->composed, base - CELL_COMB_CHARS_LO) + : NULL; + + uint32_t key; + + if (composed != NULL) { + base = composed->chars[0]; + last = composed->chars[composed->count - 1]; + key = composed_key_from_key(composed->key, wc); + } else + key = composed_key_from_key(base, wc); + +#if defined(FOOT_GRAPHEME_CLUSTERING) + if (grapheme_clustering) { + /* Check if we're on a grapheme cluster break */ + if (utf8proc_grapheme_break_stateful( + last, wc, &term->vt.grapheme_state)) + { + term_reset_grapheme_state(term); + goto out; + } + } +#endif + + int base_width = c32width(base); + if (base_width > 0) { + term->grid->cursor.point.col = col; + term->grid->cursor.lcf = false; + insert_mode_disable = true; + + if (composed == NULL) { + bool base_from_primary; + bool comb_from_primary; + bool pre_from_primary; + + char32_t precomposed = term->fonts[0] != NULL + ? fcft_precompose( + term->fonts[0], base, wc, &base_from_primary, + &comb_from_primary, &pre_from_primary) + : (char32_t)-1; + + int precomposed_width = c32width(precomposed); + + /* + * Only use the pre-composed character if: + * + * 1. we *have* a pre-composed character + * 2. the width matches the base characters width + * 3. it's in the primary font, OR one of the base or + * combining characters are *not* from the primary + * font + */ + + if (precomposed != (char32_t)-1 && + precomposed_width == base_width && + (pre_from_primary || + !base_from_primary || + !comb_from_primary)) + { + wc = precomposed; + width = precomposed_width; + term_reset_grapheme_state(term); + goto out; + } + } + + size_t wanted_count = composed != NULL ? composed->count + 1 : 2; + if (wanted_count > 255) { + xassert(composed != NULL); + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + LOG_WARN("combining character overflow:"); + LOG_WARN(" base: 0x%04x", composed->chars[0]); + for (size_t i = 1; i < composed->count; i++) + LOG_WARN(" cc: 0x%04x", composed->chars[i]); + LOG_ERR(" new: 0x%04x", wc); +#endif + /* This is going to break anyway... */ + wanted_count--; + } + + xassert(wanted_count <= 255); + + /* Check if we already have a match for the entire compose chain */ + const struct composed *cc = + composed_lookup_without_collision( + term->composed, &key, + composed != NULL ? composed->chars : &(char32_t){base}, + composed != NULL ? composed->count : 1, + wc, 0); + + if (cc != NULL) { + /* We *do* have a match! */ + wc = CELL_COMB_CHARS_LO + cc->key; + width = cc->width; + goto out; + } else { + /* No match - allocate a new chain below */ + } + + if (unlikely(term->composed_count >= + (CELL_COMB_CHARS_HI - CELL_COMB_CHARS_LO))) + { + /* We reached our maximum number of allowed composed + * character chains. Fall through here and print the + * current zero-width character to the current cell */ + LOG_WARN("maximum number of composed characters reached"); + term_reset_grapheme_state(term); + goto out; + } + + /* Allocate new chain */ + struct composed *new_cc = xmalloc(sizeof(*new_cc)); + new_cc->chars = xmalloc(wanted_count * sizeof(new_cc->chars[0])); + new_cc->key = key; + new_cc->count = wanted_count; + new_cc->chars[0] = base; + new_cc->chars[wanted_count - 1] = wc; + new_cc->forced_width = composed != NULL ? composed->forced_width : 0; + + if (composed != NULL) { + memcpy(&new_cc->chars[1], &composed->chars[1], + (wanted_count - 2) * sizeof(new_cc->chars[0])); + } + + const int grapheme_width = + composed != NULL ? composed->width : base_width; + + switch (term->conf->tweak.grapheme_width_method) { + case GRAPHEME_WIDTH_MAX: + new_cc->width = max(grapheme_width, width); + break; + + case GRAPHEME_WIDTH_DOUBLE: + new_cc->width = min(grapheme_width + width, 2); + +#if defined(FOOT_GRAPHEME_CLUSTERING) + /* Handle VS-15 and VS-16 variation selectors */ + if (unlikely(grapheme_clustering && + (wc == 0xfe0e || wc == 0xfe0f) && + new_cc->count == 2)) + { + const struct emoji_vs *vs = + bsearch( + &(struct emoji_vs){.start = new_cc->chars[0]}, + emoji_vs, sizeof(emoji_vs) / sizeof(emoji_vs[0]), + sizeof(struct emoji_vs), + &emoji_vs_compare); + + if (vs != NULL) { + xassert(new_cc->chars[0] >= vs->start && + new_cc->chars[0] <= vs->end); + + /* Force a grapheme width of 1 for VS-15, and 2 for VS-16 */ + if (wc == 0xfe0e) { + if (vs->vs15) + new_cc->width = 1; + } else if (wc == 0xfe0f) { + if (vs->vs16) + new_cc->width = 2; + } + } + } +#endif + + break; + + case GRAPHEME_WIDTH_WCSWIDTH: + new_cc->width = grapheme_width + width; + break; + } + + term->composed_count++; + composed_insert(&term->composed, new_cc); + + wc = CELL_COMB_CHARS_LO + new_cc->key; + width = new_cc->forced_width > 0 ? new_cc->forced_width : new_cc->width; + + xassert(wc >= CELL_COMB_CHARS_LO); + xassert(wc <= CELL_COMB_CHARS_HI); + goto out; + } + } else + term_reset_grapheme_state(term); + + +out: + if (width > 0) + term_print(term, wc, width, insert_mode_disable); +} + +enum term_surface +term_surface_kind(const struct terminal *term, const struct wl_surface *surface) +{ + if (likely(surface == term->window->surface.surf)) + return TERM_SURF_GRID; + else if (surface == term->window->csd.surface[CSD_SURF_TITLE].surface.surf) + return TERM_SURF_TITLE; + else if (surface == term->window->csd.surface[CSD_SURF_LEFT].surface.surf) + return TERM_SURF_BORDER_LEFT; + else if (surface == term->window->csd.surface[CSD_SURF_RIGHT].surface.surf) + return TERM_SURF_BORDER_RIGHT; + else if (surface == term->window->csd.surface[CSD_SURF_TOP].surface.surf) + return TERM_SURF_BORDER_TOP; + else if (surface == term->window->csd.surface[CSD_SURF_BOTTOM].surface.surf) + return TERM_SURF_BORDER_BOTTOM; + else if (surface == term->window->csd.surface[CSD_SURF_MINIMIZE].surface.surf) + return TERM_SURF_BUTTON_MINIMIZE; + else if (surface == term->window->csd.surface[CSD_SURF_MAXIMIZE].surface.surf) + return TERM_SURF_BUTTON_MAXIMIZE; + else if (surface == term->window->csd.surface[CSD_SURF_CLOSE].surface.surf) + return TERM_SURF_BUTTON_CLOSE; + else if (surface == term->window->tab_bar.surface.surf) + return TERM_SURF_TAB_BAR; + else if (surface == term->window->tab_overview.surface.surf) + return TERM_SURF_TAB_OVERVIEW; + else + return TERM_SURF_NONE; +} + +static bool +rows_to_text(const struct terminal *term, int start, int end, + int col_start, int col_end, char **text, size_t *len) +{ + struct extraction_context *ctx = extract_begin(SELECTION_NONE, true); + if (ctx == NULL) + return false; + + const int grid_rows = term->grid->num_rows; + int r = start; + + while (true) { + const struct row *row = term->grid->rows[r]; + xassert(row != NULL); + + const int c_end = r == end ? col_end : term->cols; + + for (int c = col_start; c < c_end; c++) { + if (!extract_one(term, row, &row->cells[c], c, ctx)) + goto out; + } + + if (r == end) + break; + + r++; + r &= grid_rows - 1; + + col_start = 0; + } + +out: + return extract_finish(ctx, text, len); +} + +bool +term_scrollback_to_text(const struct terminal *term, char **text, size_t *len) +{ + const int grid_rows = term->grid->num_rows; + int start = (term->grid->offset + term->rows) & (grid_rows - 1); + int end = (term->grid->offset + term->rows - 1) & (grid_rows - 1); + + xassert(start >= 0); + xassert(start < grid_rows); + xassert(end >= 0); + xassert(end < grid_rows); + + /* If scrollback isn't full yet, this may be NULL, so scan forward + * until we find the first non-NULL row */ + while (term->grid->rows[start] == NULL) { + start++; + start &= grid_rows - 1; + } + + while (term->grid->rows[end] == NULL) { + end--; + if (end < 0) + end += term->grid->num_rows; + } + + return rows_to_text(term, start, end, 0, term->cols, text, len); +} + +bool +term_view_to_text(const struct terminal *term, char **text, size_t *len) +{ + int start = grid_row_absolute_in_view(term->grid, 0); + int end = grid_row_absolute_in_view(term->grid, term->rows - 1); + return rows_to_text(term, start, end, 0, term->cols, text, len); +} + +bool +term_command_output_to_text(const struct terminal *term, char **text, size_t *len) +{ + int start_row = -1; + int end_row = -1; + int start_col = -1; + int end_col = -1; + + const struct grid *grid = term->grid; + const int sb_end = grid_row_absolute(grid, term->rows - 1); + const int sb_start = (sb_end + 1) & (grid->num_rows - 1); + int r = sb_end; + + while (start_row < 0) { + const struct row *row = grid->rows[r]; + if (row == NULL) + break; + + if (row->shell_integration.cmd_end >= 0) { + end_row = r; + end_col = row->shell_integration.cmd_end; + } + + if (end_row >= 0 && row->shell_integration.cmd_start >= 0) { + start_row = r; + start_col = row->shell_integration.cmd_start; + } + + if (r == sb_start) + break; + + r = (r - 1 + grid->num_rows) & (grid->num_rows - 1); + } + + if (start_row < 0) + return false; + + bool ret = rows_to_text(term, start_row, end_row, start_col, end_col, text, len); + if (!ret) + return false; + + /* + * If the FTCS_COMMAND_FINISHED marker was emitted at the *first* + * column, then the *entire* previous line is part of the command + * output. *Including* the newline, if any. + * + * Since rows_to_text() doesn't extract the column + * FTCS_COMMAND_FINISHED was emitted at (that would be wrong - + * FTCS_COMMAND_FINISHED is emitted *after* the command output, + * not at its last character), the extraction logic will not see + * the last newline (this is true for all non-line-wise selection + * types), and the extracted text will *not* end with a newline. + * + * Here we try to compensate for that. Note that if 'end_col' is + * not 0, then the command output only covers a partial row, and + * thus we do *not* want to append a newline. + */ + + if (end_col > 0) { + /* Command output covers partial row - don't append newline */ + return true; + } + + int next_to_last_row = (end_row - 1 + grid->num_rows) & (grid->num_rows - 1); + const struct row *row = grid->rows[next_to_last_row]; + + /* Add newline if last row has a hard linebreak */ + if (row->linebreak) { + char *new_text = xrealloc(*text, *len + 1 + 1); + + if (new_text == NULL) { + /* Ignore failure - use text as is (without inserting newline) */ + return true; + } + + *text = new_text; + (*len)++; + (*text)[*len - 1] = '\n'; + (*text)[*len] = '\0'; + } + + return true; +} + +bool +term_ime_is_enabled(const struct terminal *term) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + return term->ime_enabled; +#else + return false; +#endif +} + +void +term_ime_enable(struct terminal *term) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (term->ime_enabled) + return; + + LOG_DBG("IME enabled"); + + term->ime_enabled = true; + + /* IME is per seat - enable on all seat currently focusing us */ + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) + ime_enable(&it->item); + } +#endif +} + +void +term_ime_disable(struct terminal *term) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (!term->ime_enabled) + return; + + LOG_DBG("IME disabled"); + + term->ime_enabled = false; + + /* IME is per seat - disable on all seat currently focusing us */ + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) + ime_disable(&it->item); + } +#endif +} + +bool +term_ime_reset(struct terminal *term) +{ + bool at_least_one_seat_was_reset = false; + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + tll_foreach(term->wl->seats, it) { + struct seat *seat = &it->item; + + if (seat->kbd_focus != term) + continue; + + ime_reset_preedit(seat); + at_least_one_seat_was_reset = true; + } +#endif + + return at_least_one_seat_was_reset; +} + +void +term_ime_set_cursor_rect(struct terminal *term, int x, int y, int width, + int height) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + tll_foreach(term->wl->seats, it) { + if (it->item.kbd_focus == term) { + it->item.ime.cursor_rect.pending.x = x; + it->item.ime.cursor_rect.pending.y = y; + it->item.ime.cursor_rect.pending.width = width; + it->item.ime.cursor_rect.pending.height = height; + } + } +#endif +} + +void +term_osc8_open(struct terminal *term, uint64_t id, const char *uri) +{ + term_osc8_close(term); + xassert(term->vt.osc8.uri == NULL); + + term->vt.osc8.id = id; + term->vt.osc8.uri = xstrdup(uri); + + term->bits_affecting_ascii_printer.osc8 = true; + term_update_ascii_printer(term); +} + +void +term_osc8_close(struct terminal *term) +{ + free(term->vt.osc8.uri); + term->vt.osc8.uri = NULL; + term->vt.osc8.id = 0; + term->bits_affecting_ascii_printer.osc8 = false; + term_update_ascii_printer(term); +} + +void +term_set_user_mouse_cursor(struct terminal *term, const char *cursor) +{ + free(term->mouse_user_cursor); + term->mouse_user_cursor = cursor != NULL && strlen(cursor) > 0 + ? xstrdup(cursor) + : NULL; + term_xcursor_update(term); +} + +void +term_enable_size_notifications(struct terminal *term) +{ + /* Note: always send current size upon activation, regardless of + previous state */ + term->size_notifications = true; + term_send_size_notification(term); +} + +void +term_disable_size_notifications(struct terminal *term) +{ + if (!term->size_notifications) + return; + + term->size_notifications = false; +} + +void +term_send_size_notification(struct terminal *term) +{ + if (!term->size_notifications) + return; + + const int height = term->height - term->margins.top - term->margins.bottom; + const int width = term->width - term->margins.left - term->margins.right; + + char buf[128]; + const size_t n = xsnprintf( + buf, sizeof(buf), "\033[48;%d;%d;%d;%dt", + term->rows, term->cols, height, width); + term_to_slave(term, buf, n); +} + +void +term_theme_switch_to_dark(struct terminal *term) +{ + if (term->colors.active_theme == COLOR_THEME_DARK) + return; + + term_theme_apply(term, &term->conf->colors_dark); + term->colors.active_theme = COLOR_THEME_DARK; + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;1n", 9); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); +} + +void +term_theme_switch_to_light(struct terminal *term) +{ + if (term->colors.active_theme == COLOR_THEME_LIGHT) + return; + + term_theme_apply(term, &term->conf->colors_light); + term->colors.active_theme = COLOR_THEME_LIGHT; + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;2n", 9); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); +} + +void +term_theme_toggle(struct terminal *term) +{ + if (term->colors.active_theme == COLOR_THEME_DARK) { + term_theme_apply(term, &term->conf->colors_light); + term->colors.active_theme = COLOR_THEME_LIGHT; + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;2n", 9); + } else { + term_theme_apply(term, &term->conf->colors_dark); + term->colors.active_theme = COLOR_THEME_DARK; + + if (term->report_theme_changes) + term_to_slave(term, "\033[?997;1n", 9); + } + + wayl_win_alpha_changed(term->window); + term_font_subpixel_changed(term); + + term_damage_view(term); + term_damage_margins(term); + render_refresh(term); +} + +const struct color_theme * +term_theme_get(const struct terminal *term) +{ + return term->colors.active_theme == COLOR_THEME_DARK + ? &term->conf->colors_dark + : &term->conf->colors_light; +} diff --git a/terminal.h b/terminal.h new file mode 100644 index 0000000..985d2ae --- /dev/null +++ b/terminal.h @@ -0,0 +1,1042 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#if defined(FOOT_GRAPHEME_CLUSTERING) + #include +#endif + +#include +#include + +#include "composed.h" +#include "config.h" +#include "debug.h" +#include "fdm.h" +#include "key-binding.h" +#include "macros.h" +#include "notify.h" +#include "reaper.h" +#include "shm.h" +#include "wayland.h" + +enum color_source { + COLOR_DEFAULT, + COLOR_BASE16, + COLOR_BASE256, + COLOR_RGB, +}; + +/* + * Note: we want the cells to be as small as possible. Larger cells + * means fewer scrollback lines (or performance drops due to cache + * misses) + * + * Note that the members are laid out optimized for x86 + */ +struct attributes { + bool bold:1; + bool dim:1; + bool italic:1; + bool underline:1; + bool strikethrough:1; + bool blink:1; + bool conceal:1; + bool reverse:1; + uint32_t fg:24; + + bool clean:1; + enum color_source fg_src:2; + enum color_source bg_src:2; + bool confined:1; + bool selected:1; + bool url:1; + uint32_t bg:24; +}; +static_assert(sizeof(struct attributes) == 8, "VT attribute struct too large"); + +/* Last valid Unicode code point is 0x0010FFFFul */ +#define CELL_COMB_CHARS_LO 0x00200000ul +#define CELL_COMB_CHARS_HI (CELL_COMB_CHARS_LO + 0x3fffffff) +#define CELL_SPACER (CELL_COMB_CHARS_HI + 1) + +struct cell { + char32_t wc; + struct attributes attrs; +}; +static_assert(sizeof(struct cell) == 12, "bad size"); + +struct scroll_region { + int start; + int end; +}; + +struct coord { + int col; + int row; +}; + +struct range { + struct coord start; + struct coord end; +}; + +struct cursor { + struct coord point; + bool lcf; /* Last Column Flag; https://github.com/mattiase/wraptest#basic-vt-line-wrapping-rules */ +}; + +enum damage_type {DAMAGE_SCROLL, DAMAGE_SCROLL_REVERSE, + DAMAGE_SCROLL_IN_VIEW, DAMAGE_SCROLL_REVERSE_IN_VIEW}; + +struct damage { + enum damage_type type; + struct scroll_region region; + uint16_t lines; +}; + +struct uri_range_data { + uint64_t id; + char *uri; +}; + +struct underline_range_data { + enum underline_style style; + enum color_source color_src; + uint32_t color; +}; + +union row_range_data { + struct uri_range_data uri; + struct underline_range_data underline; +}; + +struct row_range { + int start; + int end; + + union { + /* This is just an expanded union row_range_data, but + * anonymous, so that we don't have to write range->u.uri.id, + * but can instead do range->uri.id */ + union { + struct uri_range_data uri; + struct underline_range_data underline; + }; + union row_range_data data; + }; +}; + +struct row_ranges { + struct row_range *v; + int size; + int count; +}; + +enum row_range_type {ROW_RANGE_URI, ROW_RANGE_UNDERLINE}; + +struct row_data { + struct row_ranges uri_ranges; + struct row_ranges underline_ranges; +}; + +struct row { + struct cell *cells; + struct row_data *extra; + + bool dirty; + bool linebreak; + + struct { + bool prompt_marker; + int cmd_start; /* Column, -1 if unset */ + int cmd_end; /* Column, -1 if unset */ + } shell_integration; +}; + +struct sixel { + /* + * These three members reflect the "current", maybe scaled version + * of the image. + * + * The values will either be NULL/-1/-1, or match either the + * values in "original", or "scaled". + * + * They are typically reset when we need to invalidate the cached + * version (e.g. when the cell dimensions change). + */ + pixman_image_t *pix; + int width; + int height; + + int rows; + int cols; + struct coord pos; + bool opaque; + + /* + * We store the cell dimensions of the time the sixel was emitted. + * + * If the font size is changed, we rescale the image accordingly, + * to ensure it stays within its cell boundaries. 'scaled' is a + * cached, rescaled version of 'data' + 'pix'. + */ + int cell_width; + int cell_height; + + struct { + void *data; + pixman_image_t *pix; + int width; + int height; + } original; + + struct { + void *data; + pixman_image_t *pix; + int width; + int height; + } scaled; +}; + +enum kitty_kbd_flags { + KITTY_KBD_DISAMBIGUATE = 0x01, + KITTY_KBD_REPORT_EVENT = 0x02, + KITTY_KBD_REPORT_ALTERNATE = 0x04, + KITTY_KBD_REPORT_ALL = 0x08, + KITTY_KBD_REPORT_ASSOCIATED = 0x10, + KITTY_KBD_SUPPORTED = (KITTY_KBD_DISAMBIGUATE | + KITTY_KBD_REPORT_EVENT | + KITTY_KBD_REPORT_ALTERNATE | + KITTY_KBD_REPORT_ALL | + KITTY_KBD_REPORT_ASSOCIATED), +}; + +struct grid { + int num_rows; + int num_cols; + int offset; + int view; + + /* + * Note: the cursor (not the *saved* cursor) could most likely be + * global state in the term struct. + * + * However, we have grid specific functions that does not have + * access to the owning term struct, but does need access to the + * cursor. + */ + struct cursor cursor; + struct cursor saved_cursor; + + struct row **rows; + struct row *cur_row; + + tll(struct damage) scroll_damage; + tll(struct sixel) sixel_images; + + struct { + enum kitty_kbd_flags flags[8]; + uint8_t idx; + } kitty_kbd; + +}; + +struct vt_subparams { + uint8_t idx; + unsigned *cur; + unsigned value[16]; + unsigned dummy; +}; + +struct vt_param { + unsigned value; + struct vt_subparams sub; +}; + +struct vt { + int state; /* enum state */ + char32_t last_printed; +#if defined(FOOT_GRAPHEME_CLUSTERING) + utf8proc_int32_t grapheme_state; +#endif + char32_t utf8; + struct { + uint8_t idx; + struct vt_param *cur; + struct vt_param v[16]; + struct vt_param dummy; + } params; + + uint32_t private; /* LSB=priv0, MSB=priv3 */ + + struct attributes attrs; + struct attributes saved_attrs; + + struct { + uint8_t *data; + size_t size; + size_t idx; + bool bel; /* true if OSC string was terminated by BEL */ + } osc; + + /* Start coordinate for current OSC-8 URI */ + struct { + uint64_t id; + char *uri; + } osc8; + + struct underline_range_data underline; + + struct { + uint8_t *data; + size_t size; + size_t idx; + void (*put_handler)(struct terminal *term, uint8_t c); + void (*unhook_handler)(struct terminal *term); + } dcs; +}; + +enum cursor_origin { ORIGIN_ABSOLUTE, ORIGIN_RELATIVE }; +enum cursor_keys { CURSOR_KEYS_DONTCARE, CURSOR_KEYS_NORMAL, CURSOR_KEYS_APPLICATION }; +enum keypad_keys { KEYPAD_DONTCARE, KEYPAD_NUMERICAL, KEYPAD_APPLICATION }; +enum charset { CHARSET_ASCII, CHARSET_GRAPHIC }; +enum charset_designator { G0, G1, G2, G3 }; + +struct charsets { + enum charset_designator selected; + enum charset_designator saved; + enum charset set[4]; /* G0-G3 */ +}; + +/* *What* to report */ +enum mouse_tracking { + MOUSE_NONE, + MOUSE_X10, /* ?9h */ + MOUSE_CLICK, /* ?1000h - report mouse clicks */ + MOUSE_DRAG, /* ?1002h - report clicks and drag motions */ + MOUSE_MOTION, /* ?1003h - report clicks and motion */ +}; + +/* *How* to report */ +enum mouse_reporting { + MOUSE_NORMAL, + MOUSE_UTF8, /* ?1005h */ + MOUSE_SGR, /* ?1006h */ + MOUSE_URXVT, /* ?1015h */ + MOUSE_SGR_PIXELS, /* ?1016h */ +}; + +enum selection_kind { + SELECTION_NONE, + SELECTION_CHAR_WISE, + SELECTION_WORD_WISE, + SELECTION_QUOTE_WISE, + SELECTION_LINE_WISE, + SELECTION_BLOCK +}; +enum selection_direction {SELECTION_UNDIR, SELECTION_LEFT, SELECTION_RIGHT}; +enum selection_scroll_direction {SELECTION_SCROLL_NOT, SELECTION_SCROLL_UP, SELECTION_SCROLL_DOWN}; +enum search_direction { SEARCH_BACKWARD_SAME_POSITION, SEARCH_BACKWARD, SEARCH_FORWARD }; +enum search_case_mode { SEARCH_CASE_SMART, SEARCH_CASE_SENSITIVE, SEARCH_CASE_INSENSITIVE }; + +struct ptmx_buffer { + void *data; + size_t len; + size_t idx; +}; + +enum term_surface { + TERM_SURF_NONE, + TERM_SURF_GRID, + TERM_SURF_TITLE, + TERM_SURF_BORDER_LEFT, + TERM_SURF_BORDER_RIGHT, + TERM_SURF_BORDER_TOP, + TERM_SURF_BORDER_BOTTOM, + TERM_SURF_BUTTON_MINIMIZE, + TERM_SURF_BUTTON_MAXIMIZE, + TERM_SURF_BUTTON_CLOSE, + TERM_SURF_TAB_BAR, + TERM_SURF_TAB_OVERVIEW, +}; + +enum overlay_style { + OVERLAY_NONE, + OVERLAY_SEARCH, + OVERLAY_FLASH, + OVERLAY_UNICODE_MODE, +}; + +typedef tll(struct ptmx_buffer) ptmx_buffer_list_t; + +enum url_action { URL_ACTION_COPY, URL_ACTION_LAUNCH, URL_ACTION_PERSISTENT }; +struct url { + uint64_t id; + char *url; + char32_t *key; + struct range range; + enum url_action action; + bool url_mode_dont_change_url_attr; /* Entering/exiting URL mode doesn't touch the cells' attr.url */ + bool osc8; + bool duplicate; +}; +typedef tll(struct url) url_list_t; + + +struct colors { + uint32_t fg; + uint32_t bg; + uint32_t table[256]; + uint16_t alpha; + uint32_t cursor_fg; /* Text color */ + uint32_t cursor_bg; /* cursor color */ + uint32_t selection_fg; + uint32_t selection_bg; + enum which_color_theme active_theme; +}; + +struct terminal { + struct fdm *fdm; + struct reaper *reaper; + const struct config *conf; + bool is_tab; /* Secondary tab terminal (shares window with primary) */ + + void (*ascii_printer)(struct terminal *term, char32_t c); + union { + struct { + bool sixels:1; + bool osc8:1; + bool underline_style:1; + bool underline_color:1; + bool insert_mode:1; + bool charset:1; + }; + uint8_t value; + } bits_affecting_ascii_printer; + + pid_t slave; + int ptmx; + + struct vt vt; + struct grid *grid; + struct grid normal; + struct grid alt; + + int cols; /* number of columns */ + int rows; /* number of rows */ + struct scroll_region scroll_region; + + struct charsets charsets; + struct charsets saved_charsets; /* For save/restore cursor + attributes */ + + bool auto_margin; + bool insert_mode; + bool reverse; + bool hide_cursor; + bool reverse_wrap; + bool bracketed_paste; + bool focus_events; + bool alt_scrolling; + bool modify_other_keys_2; /* True when modifyOtherKeys=2 (i.e. "CSI >4;2m") */ + enum cursor_origin origin; + enum cursor_keys cursor_keys_mode; + enum keypad_keys keypad_keys_mode; + enum mouse_tracking mouse_tracking; + enum mouse_reporting mouse_reporting; + char *mouse_user_cursor; /* For OSC-22 */ + + tll(int) tab_stops; + + size_t composed_count; + struct composed *composed; + + /* Temporary: for FDM */ + struct { + bool is_armed; + int lower_fd; + int upper_fd; + } delayed_render_timer; + + struct fcft_font *fonts[4]; + struct config_font *font_sizes[4]; + struct pt_or_px font_line_height; + float font_dpi; + float font_dpi_before_unmap; + bool font_is_sized_by_dpi; + int16_t font_x_ofs; + int16_t font_y_ofs; + int16_t font_baseline; + enum fcft_subpixel font_subpixel; + + struct { + struct fcft_glyph **box_drawing; + struct fcft_glyph **braille; + struct fcft_glyph **octants; + struct fcft_glyph **legacy; + + #define GLYPH_BOX_DRAWING_FIRST 0x2500 + #define GLYPH_BOX_DRAWING_LAST 0x259F + #define GLYPH_BOX_DRAWING_COUNT \ + (GLYPH_BOX_DRAWING_LAST - GLYPH_BOX_DRAWING_FIRST + 1) + + #define GLYPH_BRAILLE_FIRST 0x2800 + #define GLYPH_BRAILLE_LAST 0x28FF + #define GLYPH_BRAILLE_COUNT \ + (GLYPH_BRAILLE_LAST - GLYPH_BRAILLE_FIRST + 1) + + #define GLYPH_OCTANTS_FIRST 0x1CD00 + #define GLYPH_OCTANTS_LAST 0x1CDE5 + #define GLYPH_OCTANTS_COUNT \ + (GLYPH_OCTANTS_LAST - GLYPH_OCTANTS_FIRST + 1) + + #define GLYPH_LEGACY_FIRST 0x1FB00 + #define GLYPH_LEGACY_LAST 0x1FB9B + #define GLYPH_LEGACY_COUNT \ + (GLYPH_LEGACY_LAST - GLYPH_LEGACY_FIRST + 1) + } custom_glyphs; + + bool is_sending_paste_data; + ptmx_buffer_list_t ptmx_buffers; + ptmx_buffer_list_t ptmx_paste_buffers; + + struct { + bool esc_prefix; + bool eight_bit; + } meta; + + bool num_lock_modifier; + bool bell_action_enabled; + bool report_theme_changes; + + /* Saved DECSET modes - we save the SET state */ + struct { + bool origin:1; + bool application_cursor_keys:1; + bool application_keypad_keys:1; + bool reverse:1; + bool show_cursor:1; + bool reverse_wrap:1; + bool auto_margin:1; + bool cursor_blink:1; + bool bracketed_paste:1; + bool focus_events:1; + bool alt_scrolling:1; + //bool mouse_x10:1; + bool mouse_click:1; + bool mouse_drag:1; + bool mouse_motion:1; + //bool mouse_utf8:1; + bool mouse_sgr:1; + bool mouse_urxvt:1; + bool mouse_sgr_pixels:1; + bool meta_eight_bit:1; + bool meta_esc_prefix:1; + bool num_lock_modifier:1; + bool bell_action_enabled:1; + bool alt_screen:1; + bool ime:1; + bool app_sync_updates:1; + bool grapheme_shaping:1; + bool report_theme_changes:1; + + bool size_notifications:1; + + bool sixel_display_mode:1; + bool sixel_private_palette:1; + bool sixel_cursor_right_of_graphics:1; + } xtsave; + + bool window_title_has_been_set; + char *window_title; + tll(char *) window_title_stack; + + /* Tab activity: set when this terminal receives PTY output while + * not the active tab; cleared on tab switch. */ + bool has_unread; + //char *window_icon; /* No escape sequence available to set the icon */ + //tll(char *)window_icon_stack; + char *app_id; + + struct { + bool active; + int fd; + } flash; + + struct { + enum { BLINK_ON, BLINK_OFF } state; + int fd; + } blink; + + float scale; + float scale_before_unmap; /* Last scaling factor used */ + int width; /* pixels */ + int height; /* pixels */ + int stashed_width; + int stashed_height; + struct { + int left; + int right; + int top; + int bottom; + } margins; + int cell_width; /* pixels per cell, x-wise */ + int cell_height; /* pixels per cell, y-wise */ + + struct colors colors; + + struct { + struct colors *stack; + size_t idx; + size_t size; + } color_stack; + + enum cursor_style cursor_style; + struct { + bool decset; /* Blink enabled via '\E[?12h' */ + bool deccsusr; /* Blink enabled via '\E[X q' */ + int fd; + enum { CURSOR_BLINK_ON, CURSOR_BLINK_OFF } state; + } cursor_blink; + + struct { + enum selection_kind kind; + enum selection_direction direction; + struct range coords; + bool ongoing; + bool spaces_only; /* SELECTION_SEMANTIC_WORD */ + + struct range pivot; + + struct { + int fd; + int col; + enum selection_scroll_direction direction; + } auto_scroll; + } selection; + + bool is_searching; + struct { + char32_t *buf; + size_t len; + size_t sz; + size_t cursor; + + int original_view; + bool view_followed_offset; + struct coord match; + size_t match_len; + + /* Counter: position of current match within total matches */ + size_t total_count; + size_t current_idx; /* 1-based; 0 means no current */ + + /* Toggles */ + enum search_case_mode case_mode; + bool whole_word; + bool regex; + + /* Wrap indicator: set transiently when find wraps around */ + bool wrapped; + + /* Compiled regex (when regex == true and pattern is valid) */ + void *regex_compiled; /* regex_t* or NULL */ + bool regex_valid; + + /* History */ + struct search_history_entry { + char32_t *buf; + size_t len; + struct search_history_entry *prev, *next; + } *history_head, *history_tail; + struct search_history_entry *history_pos; /* NULL = at fresh entry */ + + struct { + char32_t *buf; + size_t len; + } last; + } search; + + struct wayland *wl; + struct wl_window *window; + bool visual_focus; + bool kbd_focus; + enum term_surface active_surface; + + struct { + struct { + struct buffer_chain *grid; + struct buffer_chain *search; + struct buffer_chain *scrollback_indicator; + struct buffer_chain *render_timer; + struct buffer_chain *url; + struct buffer_chain *csd; + struct buffer_chain *overlay; + struct buffer_chain *tab_bar; + struct buffer_chain *tab_overview; + } chains; + + /* Scheduled for rendering, as soon-as-possible */ + struct { + bool grid; + bool csd; + bool search; + bool urls; + } refresh; + + /* Scheduled for rendering, in the next frame callback */ + struct { + bool grid; + bool csd; + bool search; + bool urls; + } pending; + + bool margins; /* Someone explicitly requested a refresh of the margins */ + bool urgency; /* Signal 'urgency' (paint borders red) */ + + struct { + struct timespec last_update; + int timer_fd; + } title; + + struct { + struct timespec last_update; + int timer_fd; + } icon; + + struct { + struct timespec last_update; + int timer_fd; + } app_id; + + uint32_t scrollback_lines; /* Number of scrollback lines, from conf (TODO: move out from render struct?) */ + + struct { + bool enabled; + int timer_fd; + } app_sync_updates; + + /* Render threads + synchronization primitives */ + struct { + uint16_t count; + sem_t start; + sem_t done; + mtx_t lock; + tll(int) queue; + thrd_t *threads; + struct buffer *buf; + + struct { + mtx_t lock; + cnd_t cond; + struct buffer *buf; + struct timespec start; + struct timespec stop; + } preapplied_damage; + } workers; + + /* Last rendered cursor position */ + struct { + struct row *row; + int col; + bool hidden; + } last_cursor; + + struct buffer *last_buf; /* Buffer we rendered to last time */ + size_t frames_since_last_immediate_release; + bool preapply_last_frame_damage; + + enum overlay_style last_overlay_style; + struct buffer *last_overlay_buf; + pixman_region32_t last_overlay_clip; + + size_t search_glyph_offset; + + struct timespec input_time; + } render; + + struct { + struct grid *grid; /* Original 'normal' grid, before resize started */ + int old_screen_rows; /* term->rows before resize started */ + int old_cols; /* term->cols before resize started */ + int old_hide_cursor; /* term->hide_cursor before resize started */ + int new_rows; /* New number of scrollback rows */ + struct range selection_coords; + } interactive_resizing; + + struct { + enum { + SIXEL_DECSIXEL, /* DECSIXEL body part ", $, -, ? ... ~ */ + SIXEL_DECGRA, /* DECGRA Set Raster Attributes " Pan; Pad; Ph; Pv */ + SIXEL_DECGRI, /* DECGRI Graphics Repeat Introducer ! Pn Ch */ + SIXEL_DECGCI, /* DECGCI Graphics Color Introducer # Pc; Pu; Px; Py; Pz */ + } state; + + struct coord pos; /* Current sixel coordinate */ + int color_idx; /* Current palette index */ + uint32_t *private_palette; /* Private palette, used when private mode 1070 is enabled */ + uint32_t *shared_palette; /* Shared palette, used when private mode 1070 is disabled */ + uint32_t *palette; /* Points to either private_palette or shared_palette */ + uint32_t color; + + struct { + uint32_t *data; /* Raw image data, in ARGB */ + uint32_t *p; /* Pointer into data, for current position */ + int width; /* Image width, in pixels */ + int height; /* Image height, in pixels */ + int alloc_height; + unsigned int bottom_pixel; + } image; + + /* + * Pan is the vertical shape of a pixel + * Pad is the horizontal shape of a pixel + * + * pan/pad is the sixel's aspect ratio + */ + int pan; + int pad; + + bool scrolling:1; /* Private mode 80 */ + bool use_private_palette:1; /* Private mode 1070 */ + bool cursor_right_of_graphics:1; /* Private mode 8452 */ + + unsigned params[5]; /* Collected parameters, for RASTER, COLOR_SPEC */ + unsigned param; /* Currently collecting parameter, for RASTER, COLOR_SPEC and REPEAT */ + unsigned param_idx; /* Parameters seen */ + unsigned repeat_count; + + bool transparent_bg; + + bool linear_blending; + bool use_10bit; + pixman_format_code_t pixman_fmt; + + /* Application configurable */ + unsigned palette_size; /* Number of colors in palette */ + unsigned max_width; /* Maximum image width, in pixels */ + unsigned max_height; /* Maximum image height, in pixels */ + } sixel; + + /* TODO: wrap in a struct */ + url_list_t urls; + char32_t url_keys[5]; + bool urls_show_uri_on_jump_label; + struct grid *url_grid_snapshot; + bool ime_reenable_after_url_mode; + const struct config_spawn_template *url_launch; + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + bool ime_enabled; +#endif + + struct { + bool active; + int count; + char32_t character; + } unicode_mode; + + struct { + bool in_progress; + bool client_has_terminated; + bool fdm_done; + int terminate_timeout_fd; + int exit_status; + int next_signal; + + void (*cb)(void *data, int exit_code); + void *cb_data; + } shutdown; + + /* State, to handle chunked notifications */ + struct notification kitty_notification; + + /* Currently active notifications, from foot's perspective (their + notification helper processes are still running) */ + tll(struct notification) active_notifications; + struct notification_icon notification_icons[32]; + + char *foot_exe; + char *cwd; + + bool grapheme_shaping; + bool size_notifications; +}; + +struct config; +struct terminal *term_init( + const struct config *conf, struct fdm *fdm, struct reaper *reaper, + struct wayland *wayl, const char *foot_exe, const char *cwd, + const char *token, const char *pty_path, + int argc, char *const *argv, const char *const *envp, + void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data); + +struct terminal *term_tab_new( + struct terminal *primary, + int argc, char *const *argv, const char *const *envp, + void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data); + +void term_tab_switch(struct wl_window *win, size_t idx); +void term_tab_close(struct terminal *term); +size_t term_tab_bar_hit_test(const struct terminal *term, int x, int y); + +bool term_shutdown(struct terminal *term); +int term_destroy(struct terminal *term); + +void term_update_ascii_printer(struct terminal *term); +void term_single_shift(struct terminal *term, enum charset_designator idx); + +void term_reset(struct terminal *term, bool hard); +bool term_to_slave(struct terminal *term, const void *data, size_t len); +bool term_paste_data_to_slave( + struct terminal *term, const void *data, size_t len); + +bool term_fractional_scaling(const struct terminal *term); +bool term_preferred_buffer_scale(const struct terminal *term); +bool term_update_scale(struct terminal *term); +bool term_font_size_increase(struct terminal *term); +bool term_font_size_decrease(struct terminal *term); +bool term_font_size_reset(struct terminal *term); +bool term_font_dpi_changed(struct terminal *term, float old_scale); +void term_font_subpixel_changed(struct terminal *term); +int term_font_baseline(const struct terminal *term); + +int term_pt_or_px_as_pixels( + const struct terminal *term, const struct pt_or_px *pt_or_px); + + +void term_window_configured(struct terminal *term); + +void term_damage_rows(struct terminal *term, int start, int end); +void term_damage_rows_in_view(struct terminal *term, int start, int end); + +void term_damage_all(struct terminal *term); +void term_damage_view(struct terminal *term); + +void term_damage_cursor(struct terminal *term); +void term_damage_margins(struct terminal *term); +void term_damage_color(struct terminal *term, enum color_source src, int idx); + +void term_reset_view(struct terminal *term); + +void term_damage_scroll( + struct terminal *term, enum damage_type damage_type, + struct scroll_region region, int lines); + +void term_erase( + struct terminal *term, + int start_row, int start_col, + int end_row, int end_col); +void term_erase_scrollback(struct terminal *term); + +int term_row_rel_to_abs(const struct terminal *term, int row); +void term_cursor_home(struct terminal *term); +void term_cursor_to(struct terminal *term, int row, int col); +void term_cursor_col(struct terminal *term, int col); +void term_cursor_left(struct terminal *term, int count); +void term_cursor_right(struct terminal *term, int count); +void term_cursor_up(struct terminal *term, int count); +void term_cursor_down(struct terminal *term, int count); +void term_cursor_blink_update(struct terminal *term); + +void term_process_and_print_non_ascii(struct terminal *term, char32_t wc); +void term_print(struct terminal *term, char32_t wc, int width, + bool insert_mode_disable); +void term_fill(struct terminal *term, int row, int col, uint8_t c, size_t count, + bool use_sgr_attrs); + +void term_scroll(struct terminal *term, int rows); +void term_scroll_reverse(struct terminal *term, int rows); + +void term_scroll_partial( + struct terminal *term, struct scroll_region region, int rows); +void term_scroll_reverse_partial( + struct terminal *term, struct scroll_region region, int rows); + +void term_carriage_return(struct terminal *term); +void term_linefeed(struct terminal *term); +void term_reverse_index(struct terminal *term); + +void term_arm_blink_timer(struct terminal *term); + +void term_save_cursor(struct terminal *term); +void term_restore_cursor(struct terminal *term, const struct cursor *cursor); + +void term_visual_focus_in(struct terminal *term); +void term_visual_focus_out(struct terminal *term); +void term_kbd_focus_in(struct terminal *term); +void term_kbd_focus_out(struct terminal *term); +void term_mouse_down( + struct terminal *term, int button, int row, int col, + int row_pixels, int col_pixels, + bool shift, bool alt, bool ctrl); +void term_mouse_up( + struct terminal *term, int button, int row, int col, + int row_pixels, int col_pixels, + bool shift, bool alt, bool ctrl); +void term_mouse_motion( + struct terminal *term, int button, int row, int col, + int row_pixels, int col_pixels, + bool shift, bool alt, bool ctrl); +bool term_mouse_grabbed(const struct terminal *term, const struct seat *seat); +void term_xcursor_update(struct terminal *term); +void term_xcursor_update_for_seat(struct terminal *term, struct seat *seat); +void term_set_user_mouse_cursor(struct terminal *term, const char *cursor); + +void term_set_window_title(struct terminal *term, const char *title); +void term_set_app_id(struct terminal *term, const char *app_id); +const char *term_icon(const struct terminal *term); +void term_flash(struct terminal *term, unsigned duration_ms); +void term_bell(struct terminal *term); +bool term_spawn_new(const struct terminal *term); + +void term_enable_app_sync_updates(struct terminal *term); +void term_disable_app_sync_updates(struct terminal *term); + +enum term_surface term_surface_kind( + const struct terminal *term, const struct wl_surface *surface); + +bool term_scrollback_to_text( + const struct terminal *term, char **text, size_t *len); +bool term_view_to_text( + const struct terminal *term, char **text, size_t *len); +bool term_command_output_to_text( + const struct terminal *term, char **text, size_t *len); + +bool term_ime_is_enabled(const struct terminal *term); +void term_ime_enable(struct terminal *term); +void term_ime_disable(struct terminal *term); +bool term_ime_reset(struct terminal *term); +void term_ime_set_cursor_rect( + struct terminal *term, int x, int y, int width, int height); + +void term_urls_reset(struct terminal *term); +void term_collect_urls(struct terminal *term); + +void term_osc8_open(struct terminal *term, uint64_t id, const char *uri); +void term_osc8_close(struct terminal *term); + +bool term_ptmx_pause(struct terminal *term); +bool term_ptmx_resume(struct terminal *term); + +void term_enable_size_notifications(struct terminal *term); +void term_disable_size_notifications(struct terminal *term); +void term_send_size_notification(struct terminal *term); + +void term_theme_switch_to_dark(struct terminal *term); +void term_theme_switch_to_light(struct terminal *term); +void term_theme_toggle(struct terminal *term); +const struct color_theme *term_theme_get(const struct terminal *term); + +static inline void term_reset_grapheme_state(struct terminal *term) +{ +#if defined(FOOT_GRAPHEME_CLUSTERING) + term->vt.grapheme_state = 0; +#endif +} diff --git a/tests/meson.build b/tests/meson.build new file mode 100644 index 0000000..9baa064 --- /dev/null +++ b/tests/meson.build @@ -0,0 +1,8 @@ +config_test = executable( + 'test-config', + 'test-config.c', + wl_proto_headers, + link_with: [common, tokenize], + dependencies: [pixman, xkb, fontconfig, wayland_client, fcft, tllist]) + +test('config', config_test) diff --git a/tests/test-config.c b/tests/test-config.c new file mode 100644 index 0000000..16cfb2b --- /dev/null +++ b/tests/test-config.c @@ -0,0 +1,1559 @@ +#if !defined(_DEBUG) + #define _DEBUG +#endif +#undef NDEBUG + +#include "../log.h" + +#include "../config.c" + +#define ALEN(v) (sizeof(v) / sizeof((v)[0])) + +/* + * Stubs + */ + +void +user_notification_add_fmt(user_notifications_t *notifications, + enum user_notification_kind kind, + const char *fmt, ...) +{ +} + +static void +test_invalid_key(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key) +{ + ctx->key = key; + ctx->value = "value for invalid key"; + + if (parse_fun(ctx)) { + BUG("[%s].%s: did not fail to parse as expected" + "(key should be invalid)", ctx->section, ctx->key); + } +} + +static void +test_string(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, char *const *ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + const char *value; + bool invalid; + } input[] = { + {"a string", "a string"}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (!streq(*ptr, input[i].value)) { + BUG("[%s].%s=%s: set value (%s) not the expected one (%s)", + ctx->section, ctx->key, ctx->value, + *ptr, input[i].value); + } + } + } +} + +static void +test_c32string(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, char32_t *const *ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + const char32_t *value; + bool invalid; + } input[] = { + {"a string", U"a string"}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (c32cmp(*ptr, input[i].value) != 0) { + BUG("[%s].%s=%s: set value (%ls) not the expected one (%ls)", + ctx->section, ctx->key, ctx->value, + (const wchar_t *)*ptr, + (const wchar_t *)input[i].value); + } + } + } +} + +static void +test_boolean(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, const bool *ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + bool value; + bool invalid; + } input[] = { + {"1", true}, {"0", false}, + {"on", true}, {"off", false}, + {"true", true}, {"false", false}, + {"unittest-invalid-boolean-value", false, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (*ptr != input[i].value) { + BUG("[%s].%s=%s: set value (%s) not the expected one (%s)", + ctx->section, ctx->key, ctx->value, + *ptr ? "true" : "false", + input[i].value ? "true" : "false"); + } + } + } +} + +static void +test_uint16(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, const uint16_t *ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + uint16_t value; + bool invalid; + } input[] = { + {"0", 0}, {"65535", 65535}, {"65536", 0, true}, + {"abc", 0, true}, {"true", 0, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (*ptr != input[i].value) { + BUG("[%s].%s=%s: set value (%hu) not the expected one (%hu)", + ctx->section, ctx->key, ctx->value, + *ptr, input[i].value); + } + } + } +} + +static void +test_uint32(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, const uint32_t *ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + uint32_t value; + bool invalid; + } input[] = { + {"0", 0}, {"65536", 65536}, {"4294967295", 4294967295}, + {"4294967296", 0, true}, {"abc", 0, true}, {"true", 0, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (*ptr != input[i].value) { + BUG("[%s].%s=%s: set value (%u) not the expected one (%u)", + ctx->section, ctx->key, ctx->value, + *ptr, input[i].value); + } + } + } +} + +static void +test_float(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, const float *ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + float value; + bool invalid; + } input[] = { + {"0", 0}, {"0.1", 0.1}, {"1e10", 1e10}, {"-10.7", -10.7}, + {"abc", 0, true}, {"true", 0, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (*ptr != input[i].value) { + BUG("[%s].%s=%s: set value (%f) not the expected one (%f)", + ctx->section, ctx->key, ctx->value, + *ptr, input[i].value); + } + } + } +} + +static void +test_pt_or_px(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, const struct pt_or_px *ptr) +{ + ctx->key = key; + + static const struct { + const char *option_string; + struct pt_or_px value; + bool invalid; + } input[] = { + {"12", {.pt = 12}}, {"12px", {.px = 12}}, + {"unittest-invalid-pt-or-px-value", {0}, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + if (memcmp(ptr, &input[i].value, sizeof(*ptr)) != 0) { + BUG("[%s].%s=%s: " + "set value (pt=%f, px=%d) not the expected one (pt=%f, px=%d)", + ctx->section, ctx->key, ctx->value, + ptr->pt, ptr->px, + input[i].value.pt, input[i].value.px); + } + } + } +} + +static void +test_spawn_template(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, const struct config_spawn_template *ptr) +{ + static const char *const args[] = { + "command", "arg1", "arg2", "arg3 has spaces"}; + + ctx->key = key; + ctx->value = "command arg1 arg2 \"arg3 has spaces\""; + + if (!parse_fun(ctx)) + BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); + + if (ptr->argv.args == NULL) + BUG("[%s].%s=%s: argv is NULL", ctx->section, ctx->key, ctx->value); + + for (size_t i = 0; i < ALEN(args); i++) { + if (ptr->argv.args[i] == NULL || !streq(ptr->argv.args[i], args[i])) { + BUG("[%s].%s=%s: set value not the expected one: " + "mismatch of arg #%zu: expected=\"%s\", got=\"%s\"", + ctx->section, ctx->key, ctx->value, i, + args[i], ptr->argv.args[i]); + } + } + + if (ptr->argv.args[ALEN(args)] != NULL) { + BUG("[%s].%s=%s: set value not the expected one: " + "expected NULL terminator at arg #%zu, got=\"%s\"", + ctx->section, ctx->key, ctx->value, + ALEN(args), ptr->argv.args[ALEN(args)]); + } + + /* Trigger parse failure */ + ctx->value = "command with \"unterminated quote"; + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } +} + +static void +test_enum(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, size_t count, const char *enum_strings[static count], + int enum_values[static count], int *ptr) +{ + ctx->key = key; + + for (size_t i = 0; i < count; i++) { + ctx->value = enum_strings[i]; + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + + if (*ptr != enum_values[i]) { + BUG("[%s].%s=%s: set value not the expected one: expected %d, got %d", + ctx->section, ctx->key, ctx->value, enum_values[i], *ptr); + } + } + + ctx->value = "invalid-enum-value"; + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } +} + + +static void +test_color(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, bool alpha_allowed, uint32_t *ptr) +{ + ctx->key = key; + + const struct { + const char *option_string; + uint32_t color; + bool invalid; + } input[] = { + {"000000", 0}, + {"999999", 0x999999}, + {"ffffff", 0xffffff}, + {"ffffffff", 0xffffffff, !alpha_allowed}, + {"aabbccdd", 0xaabbccdd, !alpha_allowed}, + {"00", 0, true}, + {"0000", 0, true}, + {"00000", 0, true}, + {"000000000", 0, true}, + {"unittest-invalid-color", 0, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + + uint32_t color = input[i].color; + if (alpha_allowed && strlen(input[i].option_string) == 6) + color |= 0xff000000; + + if (*ptr != color) { + BUG("[%s].%s=%s: expected 0x%08x, got 0x%08x", + ctx->section, ctx->key, ctx->value, + color, *ptr); + } + } + } +} + +static void +test_two_colors(struct context *ctx, bool (*parse_fun)(struct context *ctx), + const char *key, bool alpha_allowed, + uint32_t *ptr1, uint32_t *ptr2) +{ + ctx->key = key; + + const struct { + const char *option_string; + uint32_t color1; + uint32_t color2; + bool invalid; + } input[] = { + {"000000 000000", 0, 0}, + + /* No alpha */ + {"999999 888888", 0x999999, 0x888888}, + {"ffffff aaaaaa", 0xffffff, 0xaaaaaa}, + + /* Both colors have alpha component */ + {"ffffffff 00000000", 0xffffffff, 0x00000000, !alpha_allowed}, + {"aabbccdd, ee112233", 0xaabbccdd, 0xee112233, !alpha_allowed}, + + /* Only one color has alpha component */ + {"ffffffff 112233", 0xffffffff, 0x112233, !alpha_allowed}, + {"ffffff ff112233", 0x00ffffff, 0xff112233, !alpha_allowed}, + + {"unittest-invalid-color", 0, 0, true}, + }; + + for (size_t i = 0; i < ALEN(input); i++) { + ctx->value = input[i].option_string; + if (input[i].invalid) { + if (parse_fun(ctx)) { + BUG("[%s].%s=%s: did not fail to parse as expected", + ctx->section, ctx->key, ctx->value); + } + } else { + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s: failed to parse", + ctx->section, ctx->key, ctx->value); + } + + if (*ptr1 != input[i].color1) { + BUG("[%s].%s=%s: expected 0x%08x, got 0x%08x", + ctx->section, ctx->key, ctx->value, + input[i].color1, *ptr1); + } + + if (*ptr2 != input[i].color2) { + BUG("[%s].%s=%s: expected 0x%08x, got 0x%08x", + ctx->section, ctx->key, ctx->value, + input[i].color2, *ptr2); + } + } + } +} + +static void +test_section_main(void) +{ + struct config conf = {0}; + struct context ctx = {.conf = &conf, .section = "main", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_main, "invalid-key"); + + test_string(&ctx, &parse_section_main, "shell", &conf.shell); + test_string(&ctx, &parse_section_main, "term", &conf.term); + test_string(&ctx, &parse_section_main, "app-id", &conf.app_id); + test_string(&ctx, &parse_section_main, "toplevel-tag", &conf.toplevel_tag); + test_string(&ctx, &parse_section_main, "utmp-helper", &conf.utmp_helper_path); + + test_c32string(&ctx, &parse_section_main, "word-delimiters", &conf.word_delimiters); + + test_boolean(&ctx, &parse_section_main, "login-shell", &conf.login_shell); + test_boolean(&ctx, &parse_section_main, "box-drawings-uses-font-glyphs", &conf.box_drawings_uses_font_glyphs); + test_boolean(&ctx, &parse_section_main, "locked-title", &conf.locked_title); + test_boolean(&ctx, &parse_section_main, "dpi-aware", &conf.dpi_aware); + test_boolean(&ctx, &parse_section_main, "gamma-correct-blending", &conf.gamma_correct); + test_boolean(&ctx, &parse_section_main, "uppercase-regex-insert", &conf.uppercase_regex_insert); + + test_pt_or_px(&ctx, &parse_section_main, "font-size-adjustment", &conf.font_size_adjustment.pt_or_px); /* TODO: test ‘N%’ values too */ + test_pt_or_px(&ctx, &parse_section_main, "line-height", &conf.line_height); + test_pt_or_px(&ctx, &parse_section_main, "letter-spacing", &conf.letter_spacing); + test_pt_or_px(&ctx, &parse_section_main, "horizontal-letter-offset", &conf.horizontal_letter_offset); + test_pt_or_px(&ctx, &parse_section_main, "vertical-letter-offset", &conf.vertical_letter_offset); + test_pt_or_px(&ctx, &parse_section_main, "underline-thickness", &conf.underline_thickness); + test_pt_or_px(&ctx, &parse_section_main, "strikeout-thickness", &conf.strikeout_thickness); + + test_uint16(&ctx, &parse_section_main, "resize-delay-ms", &conf.resize_delay_ms); + test_uint16(&ctx, &parse_section_main, "workers", &conf.render_worker_count); + + test_enum(&ctx, &parse_section_main, "selection-target", + 4, + (const char *[]){"none", "primary", "clipboard", "both"}, + (int []){SELECTION_TARGET_NONE, + SELECTION_TARGET_PRIMARY, + SELECTION_TARGET_CLIPBOARD, + SELECTION_TARGET_BOTH}, + (int *)&conf.selection_target); + + test_enum( + &ctx, &parse_section_main, "initial-window-mode", + 3, + (const char *[]){"windowed", "maximized", "fullscreen"}, + (int []){STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN}, + (int *)&conf.startup_mode); + + test_enum( + &ctx, &parse_section_main, "initial-color-theme", + 2, + (const char *[]){"dark", "light", "1", "2"}, + (int []){COLOR_THEME_DARK, COLOR_THEME_LIGHT, + COLOR_THEME_DARK, COLOR_THEME_LIGHT}, + (int *)&conf.initial_color_theme); + + /* TODO: font (custom) */ + /* TODO: include (custom) */ + /* TODO: bold-text-in-bright (enum/boolean) */ + /* TODO: pad (geometry + optional string)*/ + /* TODO: initial-window-size-pixels (geometry) */ + /* TODO: initial-window-size-chars (geometry) */ + + config_free(&conf); +} + +static void +test_section_security(void) +{ + struct config conf = {0}; + struct context ctx = {.conf = &conf, .section = "security", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_security, "invalid-key"); + test_enum( + &ctx, &parse_section_security, "osc52", 4, + (const char*[]){"disabled", "copy-enabled", "paste-enabled", "enabled"}, + (int []){OSC52_DISABLED, OSC52_COPY_ENABLED, OSC52_PASTE_ENABLED, OSC52_ENABLED}, + (int *)&conf.security.osc52); + + config_free(&conf); +} + +static void +test_section_bell(void) +{ + struct config conf = {0}; + struct context ctx = {.conf = &conf, .section = "bell", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_bell, "invalid-key"); + + test_boolean(&ctx, &parse_section_bell, "urgent", &conf.bell.urgent); + test_boolean(&ctx, &parse_section_bell, "notify", &conf.bell.notify); + test_boolean(&ctx, &parse_section_bell, "system", &conf.bell.system_bell); + test_boolean(&ctx, &parse_section_bell, "command-focused", + &conf.bell.command_focused); + test_spawn_template(&ctx, &parse_section_bell, "command", + &conf.bell.command); + + config_free(&conf); +} + +static void +test_section_desktop_notifications(void) +{ + struct config conf = {0}; + struct context ctx = {.conf = &conf, .section = "desktop-notifications", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_desktop_notifications, "invalid-key"); + + test_boolean(&ctx, &parse_section_desktop_notifications, "inhibit-when-focused", &conf.desktop_notifications.inhibit_when_focused); + test_spawn_template(&ctx, &parse_section_desktop_notifications, "command", &conf.desktop_notifications.command); + test_spawn_template(&ctx, &parse_section_desktop_notifications, "command-action-argument", &conf.desktop_notifications.command_action_arg); + test_spawn_template(&ctx, &parse_section_desktop_notifications, "close", &conf.desktop_notifications.close); + + config_free(&conf); +} + +static void +test_section_scrollback(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "scrollback", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_scrollback, "invalid-key"); + + test_uint32(&ctx, &parse_section_scrollback, "lines", + &conf.scrollback.lines); + test_float(&ctx, parse_section_scrollback, "multiplier", &conf.scrollback.multiplier); + + test_enum( + &ctx, &parse_section_scrollback, "indicator-position", + 3, + (const char *[]){"none", "fixed", "relative"}, + (int []){SCROLLBACK_INDICATOR_POSITION_NONE, + SCROLLBACK_INDICATOR_POSITION_FIXED, + SCROLLBACK_INDICATOR_POSITION_RELATIVE}, + (int *)&conf.scrollback.indicator.position); + + /* TODO: indicator-format (enum, sort-of) */ + + config_free(&conf); +} + +static void +test_section_url(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "url", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_url, "invalid-key"); + + test_spawn_template(&ctx, &parse_section_url, "launch", &conf.url.launch); + test_enum(&ctx, &parse_section_url, "osc8-underline", + 2, + (const char *[]){"url-mode", "always"}, + (int []){OSC8_UNDERLINE_URL_MODE, OSC8_UNDERLINE_ALWAYS}, + (int *)&conf.url.osc8_underline); + test_enum(&ctx, &parse_section_url, "style", + 6, (const char *[]){"none", "single", "double", "curly", "dotted", "dashed"}, + (int []){UNDERLINE_NONE, UNDERLINE_SINGLE, UNDERLINE_DOUBLE, UNDERLINE_CURLY, UNDERLINE_DOTTED, UNDERLINE_DASHED}, + (int *)&conf.url.style); + test_c32string(&ctx, &parse_section_url, "label-letters", &conf.url.label_letters); + + config_free(&conf); +} + +static void +test_section_cursor(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "cursor", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_cursor, "invalid-key"); + + test_enum( + &ctx, &parse_section_cursor, "style", + 3, + (const char *[]){"block", "beam", "underline"}, + (int []){CURSOR_BLOCK, CURSOR_BEAM, CURSOR_UNDERLINE}, + (int *)&conf.cursor.style); + test_enum( + &ctx, &parse_section_cursor, "unfocused-style", + 3, + (const char *[]){"unchanged", "hollow", "none"}, + (int []){CURSOR_UNFOCUSED_UNCHANGED, CURSOR_UNFOCUSED_HOLLOW, CURSOR_UNFOCUSED_NONE}, + (int *)&conf.cursor.unfocused_style); + test_boolean(&ctx, &parse_section_cursor, "blink", &conf.cursor.blink.enabled); + test_uint32(&ctx, &parse_section_cursor, "blink-rate", &conf.cursor.blink.rate_ms); + test_pt_or_px(&ctx, &parse_section_cursor, "beam-thickness", + &conf.cursor.beam_thickness); + test_pt_or_px(&ctx, &parse_section_cursor, "underline-thickness", + &conf.cursor.underline_thickness); + + /* TODO: color (two RRGGBB values) */ + + config_free(&conf); +} + +static void +test_section_mouse(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "mouse", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_mouse, "invalid-key"); + + test_boolean(&ctx, &parse_section_mouse, "hide-when-typing", + &conf.mouse.hide_when_typing); + test_boolean(&ctx, &parse_section_mouse, "alternate-scroll-mode", + &conf.mouse.alternate_scroll_mode); + + config_free(&conf); +} + +static void +test_section_touch(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "touch", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_touch, "invalid-key"); + + test_uint32(&ctx, &parse_section_touch, "long-press-delay", + &conf.touch.long_press_delay); + + config_free(&conf); +} + +static void +test_section_colors_dark(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "colors-dark", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_colors, "invalid-key"); + + test_color(&ctx, &parse_section_colors_dark, "foreground", false, &conf.colors_dark.fg); + test_color(&ctx, &parse_section_colors_dark, "background", false, &conf.colors_dark.bg); + test_color(&ctx, &parse_section_colors_dark, "regular0", false, &conf.colors_dark.table[0]); + test_color(&ctx, &parse_section_colors_dark, "regular1", false, &conf.colors_dark.table[1]); + test_color(&ctx, &parse_section_colors_dark, "regular2", false, &conf.colors_dark.table[2]); + test_color(&ctx, &parse_section_colors_dark, "regular3", false, &conf.colors_dark.table[3]); + test_color(&ctx, &parse_section_colors_dark, "regular4", false, &conf.colors_dark.table[4]); + test_color(&ctx, &parse_section_colors_dark, "regular5", false, &conf.colors_dark.table[5]); + test_color(&ctx, &parse_section_colors_dark, "regular6", false, &conf.colors_dark.table[6]); + test_color(&ctx, &parse_section_colors_dark, "regular7", false, &conf.colors_dark.table[7]); + test_color(&ctx, &parse_section_colors_dark, "bright0", false, &conf.colors_dark.table[8]); + test_color(&ctx, &parse_section_colors_dark, "bright1", false, &conf.colors_dark.table[9]); + test_color(&ctx, &parse_section_colors_dark, "bright2", false, &conf.colors_dark.table[10]); + test_color(&ctx, &parse_section_colors_dark, "bright3", false, &conf.colors_dark.table[11]); + test_color(&ctx, &parse_section_colors_dark, "bright4", false, &conf.colors_dark.table[12]); + test_color(&ctx, &parse_section_colors_dark, "bright5", false, &conf.colors_dark.table[13]); + test_color(&ctx, &parse_section_colors_dark, "bright6", false, &conf.colors_dark.table[14]); + test_color(&ctx, &parse_section_colors_dark, "bright7", false, &conf.colors_dark.table[15]); + test_color(&ctx, &parse_section_colors_dark, "dim0", false, &conf.colors_dark.dim[0]); + test_color(&ctx, &parse_section_colors_dark, "dim1", false, &conf.colors_dark.dim[1]); + test_color(&ctx, &parse_section_colors_dark, "dim2", false, &conf.colors_dark.dim[2]); + test_color(&ctx, &parse_section_colors_dark, "dim3", false, &conf.colors_dark.dim[3]); + test_color(&ctx, &parse_section_colors_dark, "dim4", false, &conf.colors_dark.dim[4]); + test_color(&ctx, &parse_section_colors_dark, "dim5", false, &conf.colors_dark.dim[5]); + test_color(&ctx, &parse_section_colors_dark, "dim6", false, &conf.colors_dark.dim[6]); + test_color(&ctx, &parse_section_colors_dark, "dim7", false, &conf.colors_dark.dim[7]); + test_color(&ctx, &parse_section_colors_dark, "selection-foreground", false, &conf.colors_dark.selection_fg); + test_color(&ctx, &parse_section_colors_dark, "selection-background", false, &conf.colors_dark.selection_bg); + test_color(&ctx, &parse_section_colors_dark, "urls", false, &conf.colors_dark.url); + test_two_colors(&ctx, &parse_section_colors_dark, "jump-labels", false, + &conf.colors_dark.jump_label.fg, + &conf.colors_dark.jump_label.bg); + test_two_colors(&ctx, &parse_section_colors_dark, "scrollback-indicator", false, + &conf.colors_dark.scrollback_indicator.fg, + &conf.colors_dark.scrollback_indicator.bg); + test_two_colors(&ctx, &parse_section_colors_dark, "search-box-no-match", false, + &conf.colors_dark.search_box.no_match.fg, + &conf.colors_dark.search_box.no_match.bg); + test_two_colors(&ctx, &parse_section_colors_dark, "search-box-match", false, + &conf.colors_dark.search_box.match.fg, + &conf.colors_dark.search_box.match.bg); + + test_two_colors(&ctx, &parse_section_colors_dark, "cursor", false, + &conf.colors_dark.cursor.text, + &conf.colors_dark.cursor.cursor); + + test_enum(&ctx, &parse_section_colors_dark, "alpha-mode", 3, + (const char *[]){"default", "matching", "all"}, + (int []){ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL}, + (int *)&conf.colors_dark.alpha_mode); + + test_enum(&ctx, &parse_section_colors_dark, "dim-blend-towards", 2, + (const char *[]){"black", "white"}, + (int []){DIM_BLEND_TOWARDS_BLACK, DIM_BLEND_TOWARDS_WHITE}, + (int *)&conf.colors_dark.dim_blend_towards); + + for (size_t i = 0; i < 255; i++) { + char key_name[4]; + sprintf(key_name, "%zu", i); + test_color(&ctx, &parse_section_colors_dark, key_name, false, + &conf.colors_dark.table[i]); + } + + test_boolean(&ctx, &parse_section_colors_dark, "blur", &conf.colors_dark.blur); + + test_invalid_key(&ctx, &parse_section_colors_dark, "256"); + + /* TODO: alpha (float in range 0-1, converted to uint16_t) */ + + config_free(&conf); +} + +static void +test_section_colors_light(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "colors-light", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_colors, "invalid-key"); + + test_color(&ctx, &parse_section_colors_light, "foreground", false, &conf.colors_light.fg); + test_color(&ctx, &parse_section_colors_light, "background", false, &conf.colors_light.bg); + test_color(&ctx, &parse_section_colors_light, "regular0", false, &conf.colors_light.table[0]); + test_color(&ctx, &parse_section_colors_light, "regular1", false, &conf.colors_light.table[1]); + test_color(&ctx, &parse_section_colors_light, "regular2", false, &conf.colors_light.table[2]); + test_color(&ctx, &parse_section_colors_light, "regular3", false, &conf.colors_light.table[3]); + test_color(&ctx, &parse_section_colors_light, "regular4", false, &conf.colors_light.table[4]); + test_color(&ctx, &parse_section_colors_light, "regular5", false, &conf.colors_light.table[5]); + test_color(&ctx, &parse_section_colors_light, "regular6", false, &conf.colors_light.table[6]); + test_color(&ctx, &parse_section_colors_light, "regular7", false, &conf.colors_light.table[7]); + test_color(&ctx, &parse_section_colors_light, "bright0", false, &conf.colors_light.table[8]); + test_color(&ctx, &parse_section_colors_light, "bright1", false, &conf.colors_light.table[9]); + test_color(&ctx, &parse_section_colors_light, "bright2", false, &conf.colors_light.table[10]); + test_color(&ctx, &parse_section_colors_light, "bright3", false, &conf.colors_light.table[11]); + test_color(&ctx, &parse_section_colors_light, "bright4", false, &conf.colors_light.table[12]); + test_color(&ctx, &parse_section_colors_light, "bright5", false, &conf.colors_light.table[13]); + test_color(&ctx, &parse_section_colors_light, "bright6", false, &conf.colors_light.table[14]); + test_color(&ctx, &parse_section_colors_light, "bright7", false, &conf.colors_light.table[15]); + test_color(&ctx, &parse_section_colors_light, "dim0", false, &conf.colors_light.dim[0]); + test_color(&ctx, &parse_section_colors_light, "dim1", false, &conf.colors_light.dim[1]); + test_color(&ctx, &parse_section_colors_light, "dim2", false, &conf.colors_light.dim[2]); + test_color(&ctx, &parse_section_colors_light, "dim3", false, &conf.colors_light.dim[3]); + test_color(&ctx, &parse_section_colors_light, "dim4", false, &conf.colors_light.dim[4]); + test_color(&ctx, &parse_section_colors_light, "dim5", false, &conf.colors_light.dim[5]); + test_color(&ctx, &parse_section_colors_light, "dim6", false, &conf.colors_light.dim[6]); + test_color(&ctx, &parse_section_colors_light, "dim7", false, &conf.colors_light.dim[7]); + test_color(&ctx, &parse_section_colors_light, "selection-foreground", false, &conf.colors_light.selection_fg); + test_color(&ctx, &parse_section_colors_light, "selection-background", false, &conf.colors_light.selection_bg); + test_color(&ctx, &parse_section_colors_light, "urls", false, &conf.colors_light.url); + test_two_colors(&ctx, &parse_section_colors_light, "jump-labels", false, + &conf.colors_light.jump_label.fg, + &conf.colors_light.jump_label.bg); + test_two_colors(&ctx, &parse_section_colors_light, "scrollback-indicator", false, + &conf.colors_light.scrollback_indicator.fg, + &conf.colors_light.scrollback_indicator.bg); + test_two_colors(&ctx, &parse_section_colors_light, "search-box-no-match", false, + &conf.colors_light.search_box.no_match.fg, + &conf.colors_light.search_box.no_match.bg); + test_two_colors(&ctx, &parse_section_colors_light, "search-box-match", false, + &conf.colors_light.search_box.match.fg, + &conf.colors_light.search_box.match.bg); + + test_two_colors(&ctx, &parse_section_colors_light, "cursor", false, + &conf.colors_light.cursor.text, + &conf.colors_light.cursor.cursor); + + test_enum(&ctx, &parse_section_colors_light, "alpha-mode", 3, + (const char *[]){"default", "matching", "all"}, + (int []){ALPHA_MODE_DEFAULT, ALPHA_MODE_MATCHING, ALPHA_MODE_ALL}, + (int *)&conf.colors_light.alpha_mode); + + test_enum(&ctx, &parse_section_colors_light, "dim-blend-towards", 2, + (const char *[]){"black", "white"}, + (int []){DIM_BLEND_TOWARDS_BLACK, DIM_BLEND_TOWARDS_WHITE}, + (int *)&conf.colors_light.dim_blend_towards); + + for (size_t i = 0; i < 255; i++) { + char key_name[4]; + sprintf(key_name, "%zu", i); + test_color(&ctx, &parse_section_colors_light, key_name, false, + &conf.colors_light.table[i]); + } + + test_boolean(&ctx, &parse_section_colors_light, "blur", &conf.colors_light.blur); + + test_invalid_key(&ctx, &parse_section_colors_light, "256"); + + /* TODO: alpha (float in range 0-1, converted to uint16_t) */ + + config_free(&conf); +} + +static void +test_section_csd(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "csd", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_csd, "invalid-key"); + + test_enum( + &ctx, &parse_section_csd, "preferred", + 3, + (const char *[]){"none", "client", "server"}, + (int []){CONF_CSD_PREFER_NONE, + CONF_CSD_PREFER_CLIENT, + CONF_CSD_PREFER_SERVER}, + (int *)&conf.csd.preferred); + test_uint16(&ctx, &parse_section_csd, "size", &conf.csd.title_height); + test_color(&ctx, &parse_section_csd, "color", true, &conf.csd.color.title); + test_uint16(&ctx, &parse_section_csd, "border-width", + &conf.csd.border_width_visible); + test_color(&ctx, &parse_section_csd, "border-color", true, + &conf.csd.color.border); + test_uint16(&ctx, &parse_section_csd, "button-width", + &conf.csd.button_width); + test_color(&ctx, &parse_section_csd, "button-color", true, + &conf.csd.color.buttons); + test_color(&ctx, &parse_section_csd, "button-minimize-color", true, + &conf.csd.color.minimize); + test_color(&ctx, &parse_section_csd, "button-maximize-color", true, + &conf.csd.color.maximize); + test_color(&ctx, &parse_section_csd, "button-close-color", true, + &conf.csd.color.quit); + test_boolean(&ctx, &parse_section_csd, "hide-when-maximized", + &conf.csd.hide_when_maximized); + test_boolean(&ctx, &parse_section_csd, "double-click-to-maximize", + &conf.csd.double_click_to_maximize); + + /* TODO: verify the ‘set’ bit is actually set for colors */ + /* TODO: font */ + + config_free(&conf); +} + +static bool +have_modifier(const config_modifier_list_t *mods, const char *mod) +{ + tll_foreach(*mods, it) { + if (strcmp(it->item, mod) == 0) + return true; + } + + return false; +} + +static void +test_key_binding(struct context *ctx, bool (*parse_fun)(struct context *ctx), + int action, int max_action, const char *const *map, + struct config_key_binding_list *bindings, + enum key_binding_type type, bool need_argv, bool need_section_id) +{ + xassert(map[action] != NULL); + xassert(bindings->count == 0); + + const char *key = map[action]; + + /* “Randomize” which modifiers to enable */ + const bool ctrl = action % 2; + const bool alt = action % 3; + const bool shift = action % 4; + const bool super = action % 5; + const bool argv = need_argv; + const bool section_id = need_section_id; + + xassert(!(argv && section_id)); + + static const char *const args[] = { + "command", "arg1", "arg2", "arg3 has spaces"}; + + /* Generate the modifier part of the ‘value’ */ + char modifier_string[32]; + sprintf(modifier_string, "%s%s%s%s", + ctrl ? XKB_MOD_NAME_CTRL "+" : "", + alt ? XKB_MOD_NAME_ALT "+" : "", + shift ? XKB_MOD_NAME_SHIFT "+" : "", + super ? XKB_MOD_NAME_LOGO "+" : ""); + + /* Use a unique symbol for this action (key bindings) */ + const xkb_keysym_t sym = XKB_KEY_a + action; + + /* Mouse button (mouse bindings) */ + const int button_idx = action % ALEN(button_map); + const int button = button_map[button_idx].code; + const int click_count = action % 3 + 1; + + /* Finally, generate the ‘value’ (e.g. “Control+shift+x”) */ + char value[128] = {0}; + + ctx->key = key; + ctx->value = value; + + /* First, try setting the empty string */ + if (parse_fun(ctx)) { + BUG("[%s].%s=: did not fail to parse as expected", + ctx->section, ctx->key); + } + + switch (type) { + case KEY_BINDING: { + char sym_name[16]; + xkb_keysym_get_name(sym, sym_name, sizeof(sym_name)); + + snprintf(value, sizeof(value), "%s%s%s", + argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : section_id ? "[foobar]" : "", + modifier_string, sym_name); + break; + } + + case MOUSE_BINDING: { + const char *const button_name = button_map[button_idx].name; + int chars = snprintf( + value, sizeof(value), "%s%s%s", + argv ? "[command arg1 arg2 \"arg3 has spaces\"] " : section_id ? "[foobar]" : "", + modifier_string, button_name); + + xassert(click_count > 0); + if (click_count > 1) + snprintf(&value[chars], sizeof(value) - chars, "-%d", click_count); + break; + } + } + + if (!parse_fun(ctx)) { + BUG("[%s].%s=%s failed to parse", + ctx->section, ctx->key, ctx->value); + } + + const struct config_key_binding *binding = + &bindings->arr[bindings->count - 1]; + + if (argv) { + if (binding->aux.pipe.args == NULL) { + BUG("[%s].%s=%s: pipe argv is NULL", + ctx->section, ctx->key, ctx->value); + } + + for (size_t i = 0; i < ALEN(args); i++) { + if (binding->aux.pipe.args[i] == NULL || + !streq(binding->aux.pipe.args[i], args[i])) + { + BUG("[%s].%s=%s: pipe argv not the expected one: " + "mismatch of arg #%zu: expected=\"%s\", got=\"%s\"", + ctx->section, ctx->key, ctx->value, i, + args[i], binding->aux.pipe.args[i]); + } + } + + if (binding->aux.pipe.args[ALEN(args)] != NULL) { + BUG("[%s].%s=%s: pipe argv not the expected one: " + "expected NULL terminator at arg #%zu, got=\"%s\"", + ctx->section, ctx->key, ctx->value, + ALEN(args), binding->aux.pipe.args[ALEN(args)]); + } + } else if (section_id) { + if (binding->aux.regex_name == NULL) { + BUG("[%s].%s=%s: regex name is NULL", + ctx->section, ctx->key, ctx->value); + } + + if (!streq(binding->aux.regex_name, "foobar")) { + BUG("[%s].%s=%s: regex name not the expected one: " + "expected=\"%s\", got=\"%s\"", + ctx->section, ctx->key, ctx->value, + "foobar", binding->aux.regex_name); + } + } else { + if (binding->aux.pipe.args != NULL) { + BUG("[%s].%s=%s: pipe argv not NULL", + ctx->section, ctx->key, ctx->value); + } + } + + if (binding->action != action) { + BUG("[%s].%s=%s: action mismatch: %d != %d", + ctx->section, ctx->key, ctx->value, binding->action, action); + } + + bool have_ctrl = have_modifier(&binding->modifiers, XKB_MOD_NAME_CTRL); + bool have_alt = have_modifier(&binding->modifiers, XKB_MOD_NAME_ALT); + bool have_shift = have_modifier(&binding->modifiers, XKB_MOD_NAME_SHIFT); + bool have_super = have_modifier(&binding->modifiers, XKB_MOD_NAME_LOGO); + + if (have_ctrl != ctrl || have_alt != alt || + have_shift != shift || have_super != super) + { + BUG("[%s].%s=%s: modifier mismatch:\n" + " have: ctrl=%d, alt=%d, shift=%d, super=%d\n" + " expected: ctrl=%d, alt=%d, shift=%d, super=%d", + ctx->section, ctx->key, ctx->value, + have_ctrl, have_alt, have_shift, have_super, + ctrl, alt, shift, super); + } + + switch (type) { + case KEY_BINDING: + if (binding->k.sym != sym) { + BUG("[%s].%s=%s: key symbol mismatch: %d != %d", + ctx->section, ctx->key, ctx->value, binding->k.sym, sym); + } + break; + + case MOUSE_BINDING:; + if (binding->m.button != button) { + BUG("[%s].%s=%s: mouse button mismatch: %d != %d", + ctx->section, ctx->key, ctx->value, binding->m.button, button); + } + + if (binding->m.count != click_count) { + BUG("[%s].%s=%s: mouse button click count mismatch: %d != %d", + ctx->section, ctx->key, ctx->value, + binding->m.count, click_count); + } + break; + } + + + free_key_binding_list(bindings); +} + +enum collision_test_mode { + FAIL_DIFFERENT_ACTION, + FAIL_DIFFERENT_ARGV, + FAIL_MOUSE_OVERRIDE, + SUCCEED_SAME_ACTION_AND_ARGV, +}; + +static void +_test_binding_collisions(struct context *ctx, + int max_action, const char *const *map, + enum key_binding_type type, + enum collision_test_mode test_mode) +{ + struct config_key_binding *bindings_array = + xcalloc(2, sizeof(bindings_array[0])); + + struct config_key_binding_list bindings = { + .count = 2, + .arr = bindings_array, + }; + + /* First, verify we get a collision when trying to assign the same + * key combo to multiple actions */ + bindings.arr[0] = (struct config_key_binding){ + .action = (test_mode == FAIL_DIFFERENT_ACTION + ? max_action - 1 : max_action), + .modifiers = tll_init(), + .path = "unittest", + }; + tll_push_back(bindings.arr[0].modifiers, xstrdup(XKB_MOD_NAME_CTRL)); + + bindings.arr[1] = (struct config_key_binding){ + .action = max_action, + .modifiers = tll_init(), + .path = "unittest", + }; + tll_push_back(bindings.arr[1].modifiers, xstrdup(XKB_MOD_NAME_CTRL)); + + switch (type) { + case KEY_BINDING: + bindings.arr[0].k.sym = XKB_KEY_a; + bindings.arr[1].k.sym = XKB_KEY_a; + break; + + case MOUSE_BINDING: + bindings.arr[0].m.button = BTN_LEFT; + bindings.arr[0].m.count = 1; + bindings.arr[1].m.button = BTN_LEFT; + bindings.arr[1].m.count = 1; + break; + } + + switch (test_mode) { + case FAIL_DIFFERENT_ACTION: + break; + + case FAIL_MOUSE_OVERRIDE: + tll_free_and_free(ctx->conf->mouse.selection_override_modifiers, free); + tll_push_back(ctx->conf->mouse.selection_override_modifiers, xstrdup(XKB_MOD_NAME_CTRL)); + break; + + case FAIL_DIFFERENT_ARGV: + case SUCCEED_SAME_ACTION_AND_ARGV: + bindings.arr[0].aux.type = BINDING_AUX_PIPE; + bindings.arr[0].aux.master_copy = true; + bindings.arr[0].aux.pipe.args = xcalloc( + 4, sizeof(bindings.arr[0].aux.pipe.args[0])); + bindings.arr[0].aux.pipe.args[0] = xstrdup("/usr/bin/foobar"); + bindings.arr[0].aux.pipe.args[1] = xstrdup("hello"); + bindings.arr[0].aux.pipe.args[2] = xstrdup("world"); + + bindings.arr[1].aux.type = BINDING_AUX_PIPE; + bindings.arr[1].aux.master_copy = true; + bindings.arr[1].aux.pipe.args = xcalloc( + 4, sizeof(bindings.arr[1].aux.pipe.args[0])); + bindings.arr[1].aux.pipe.args[0] = xstrdup("/usr/bin/foobar"); + bindings.arr[1].aux.pipe.args[1] = xstrdup("hello"); + + if (test_mode == SUCCEED_SAME_ACTION_AND_ARGV) + bindings.arr[1].aux.pipe.args[2] = xstrdup("world"); + break; + } + + bool expected_result = + test_mode == SUCCEED_SAME_ACTION_AND_ARGV ? true : false; + + if (resolve_key_binding_collisions( + ctx->conf, ctx->section, map, &bindings, type) != expected_result) + { + BUG("[%s].%s vs. %s: %s", + ctx->section, map[max_action - 1], map[max_action], + (expected_result == true + ? "invalid key combo collision detected" + : "key combo collision not detected")); + } + + if (expected_result == false) { + if (bindings.count != 1) + BUG("[%s]: colliding binding not removed", ctx->section); + + if (bindings.arr[0].action != + (test_mode == FAIL_DIFFERENT_ACTION ? max_action - 1 : max_action)) + { + BUG("[%s]: wrong binding removed", ctx->section); + } + } + + free_key_binding_list(&bindings); +} + +static void +test_binding_collisions(struct context *ctx, + int max_action, const char *const *map, + enum key_binding_type type) +{ + _test_binding_collisions(ctx, max_action, map, type, FAIL_DIFFERENT_ACTION); + _test_binding_collisions(ctx, max_action, map, type, FAIL_DIFFERENT_ARGV); + _test_binding_collisions(ctx, max_action, map, type, SUCCEED_SAME_ACTION_AND_ARGV); + + if (type == MOUSE_BINDING) { + _test_binding_collisions( + ctx, max_action, map, type, FAIL_MOUSE_OVERRIDE); + } +} + +static void +test_section_key_bindings(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "key-bindings", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_key_bindings, "invalid-key"); + + for (int action = 0; action < BIND_ACTION_KEY_COUNT; action++) { + if (binding_action_map[action] == NULL) + continue; + + test_key_binding( + &ctx, &parse_section_key_bindings, + action, BIND_ACTION_KEY_COUNT - 1, + binding_action_map, &conf.bindings.key, KEY_BINDING, + action >= BIND_ACTION_PIPE_SCROLLBACK && action <= BIND_ACTION_PIPE_COMMAND_OUTPUT, + action >= BIND_ACTION_REGEX_LAUNCH && action <= BIND_ACTION_REGEX_COPY); + } + + config_free(&conf); +} + +static void +test_section_key_bindings_collisions(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "key-bindings", .path = "unittest"}; + + test_binding_collisions( + &ctx, BIND_ACTION_KEY_COUNT - 1, binding_action_map, KEY_BINDING); + + config_free(&conf); +} + +static void +test_section_search_bindings(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "search-bindings", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_search_bindings, "invalid-key"); + + for (int action = 0; action < BIND_ACTION_SEARCH_COUNT; action++) { + if (search_binding_action_map[action] == NULL) + continue; + + test_key_binding( + &ctx, &parse_section_search_bindings, + action, BIND_ACTION_SEARCH_COUNT - 1, + search_binding_action_map, &conf.bindings.search, KEY_BINDING, + false, false); + } + + config_free(&conf); +} + +static void +test_section_search_bindings_collisions(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "search-bindings", .path = "unittest"}; + + test_binding_collisions( + &ctx, + BIND_ACTION_SEARCH_COUNT - 1, search_binding_action_map, KEY_BINDING); + + config_free(&conf); +} + +static void +test_section_url_bindings(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "rul-bindings", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_url_bindings, "invalid-key"); + + for (int action = 0; action < BIND_ACTION_URL_COUNT; action++) { + if (url_binding_action_map[action] == NULL) + continue; + + test_key_binding( + &ctx, &parse_section_url_bindings, + action, BIND_ACTION_URL_COUNT - 1, + url_binding_action_map, &conf.bindings.url, KEY_BINDING, + false, false); + } + + config_free(&conf); +} + +static void +test_section_url_bindings_collisions(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "url-bindings", .path = "unittest"}; + + test_binding_collisions( + &ctx, + BIND_ACTION_URL_COUNT - 1, url_binding_action_map, KEY_BINDING); + + config_free(&conf); +} + +static void +test_section_mouse_bindings(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "mouse-bindings", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_mouse_bindings, "invalid-key"); + + for (int action = 0; action < BIND_ACTION_COUNT; action++) { + if (binding_action_map[action] == NULL) + continue; + + test_key_binding( + &ctx, &parse_section_mouse_bindings, + action, BIND_ACTION_COUNT - 1, + binding_action_map, &conf.bindings.mouse, MOUSE_BINDING, + false, false); + } + + config_free(&conf); +} + +static void +test_section_mouse_bindings_collisions(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "mouse-bindings", .path = "unittest"}; + + test_binding_collisions( + &ctx, + BIND_ACTION_COUNT - 1, binding_action_map, MOUSE_BINDING); + + config_free(&conf); +} + +static void +test_section_text_bindings(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "text-bindings", .path = "unittest"}; + + ctx.key = "abcd"; + ctx.value = XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT "+x"; + xassert(parse_section_text_bindings(&ctx)); + + ctx.key = "\\x07"; + xassert(parse_section_text_bindings(&ctx)); + + ctx.key = "\\x1g"; + xassert(!parse_section_text_bindings(&ctx)); + + ctx.key = "\\x1"; + xassert(!parse_section_text_bindings(&ctx)); + + ctx.key = "\\x"; + xassert(!parse_section_text_bindings(&ctx)); + + ctx.key = "\\"; + xassert(!parse_section_text_bindings(&ctx)); + + ctx.key = "\\y"; + xassert(!parse_section_text_bindings(&ctx)); + +#if 0 + /* Invalid modifier and key names are detected later, when a + * layout is applied */ + ctx.key = "abcd"; + ctx.value = "InvalidMod+y"; + xassert(!parse_section_text_bindings(&ctx)); +#endif + config_free(&conf); +} + +static void +test_section_environment(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "environment", .path = "unittest"}; + + /* A single variable */ + ctx.key = "FOO"; + ctx.value = "bar"; + xassert(parse_section_environment(&ctx)); + xassert(tll_length(conf.env_vars) == 1); + xassert(streq(tll_front(conf.env_vars).name, "FOO")); + xassert(streq(tll_front(conf.env_vars).value, "bar")); + + /* Add a second variable */ + ctx.key = "BAR"; + ctx.value = "123"; + xassert(parse_section_environment(&ctx)); + xassert(tll_length(conf.env_vars) == 2); + xassert(streq(tll_back(conf.env_vars).name, "BAR")); + xassert(streq(tll_back(conf.env_vars).value, "123")); + + /* Replace the *value* of the first variable */ + ctx.key = "FOO"; + ctx.value = "456"; + xassert(parse_section_environment(&ctx)); + xassert(tll_length(conf.env_vars) == 2); + xassert(streq(tll_front(conf.env_vars).name, "FOO")); + xassert(streq(tll_front(conf.env_vars).value, "456")); + xassert(streq(tll_back(conf.env_vars).name, "BAR")); + xassert(streq(tll_back(conf.env_vars).value, "123")); + + config_free(&conf); +} + +static void +test_section_tweak(void) +{ + struct config conf = {0}; + struct context ctx = { + .conf = &conf, .section = "tweak", .path = "unittest"}; + + test_invalid_key(&ctx, &parse_section_tweak, "invalid-key"); + + test_enum( + &ctx, &parse_section_tweak, "scaling-filter", + 5, + (const char *[]){"none", "nearest", "bilinear", "cubic", "lanczos3"}, + (int []){FCFT_SCALING_FILTER_NONE, + FCFT_SCALING_FILTER_NEAREST, + FCFT_SCALING_FILTER_BILINEAR, + FCFT_SCALING_FILTER_CUBIC, + FCFT_SCALING_FILTER_LANCZOS3}, + (int *)&conf.tweak.fcft_filter); + + test_boolean(&ctx, &parse_section_tweak, "overflowing-glyphs", + &conf.tweak.overflowing_glyphs); + + test_enum( + &ctx, &parse_section_tweak, "render-timer", + 4, + (const char *[]){"none", "osd", "log", "both"}, + (int []){RENDER_TIMER_NONE, + RENDER_TIMER_OSD, + RENDER_TIMER_LOG, + RENDER_TIMER_BOTH}, + (int *)&conf.tweak.render_timer); + + test_float(&ctx, &parse_section_tweak, "box-drawing-base-thickness", + &conf.tweak.box_drawing_base_thickness); + test_boolean(&ctx, &parse_section_tweak, "box-drawing-solid-shades", + &conf.tweak.box_drawing_solid_shades); + +#if 0 /* Must be less than 16ms */ + test_uint32(&ctx, &parse_section_tweak, "delayed-render-lower", + &conf.tweak.delayed_render_lower_ns); + test_uint32(&ctx, &parse_section_tweak, "delayed-render-upper", + &conf.tweak.delayed_render_upper_ns); +#endif + test_boolean(&ctx, &parse_section_tweak, "damage-whole-window", + &conf.tweak.damage_whole_window); + +#if defined(FOOT_GRAPHEME_CLUSTERING) + test_boolean(&ctx, &parse_section_tweak, "grapheme-shaping", + &conf.tweak.grapheme_shaping); +#else + /* TODO: the setting still exists, but is always forced to ‘false’. */ +#endif + + test_enum( + &ctx, &parse_section_tweak, "grapheme-width-method", + 3, + (const char *[]){"wcswidth", "double-width", "max"}, + (int []){GRAPHEME_WIDTH_WCSWIDTH, + GRAPHEME_WIDTH_DOUBLE, + GRAPHEME_WIDTH_MAX}, + (int *)&conf.tweak.grapheme_width_method); + + test_boolean(&ctx, &parse_section_tweak, "font-monospace-warn", + &conf.tweak.font_monospace_warn); + + test_float(&ctx, &parse_section_tweak, "bold-text-in-bright-amount", + &conf.bold_in_bright.amount); + + test_uint32(&ctx, &parse_section_tweak, "min-stride-alignment", + &conf.tweak.min_stride_alignment); + +#if 0 /* Must be equal to, or less than INT32_MAX */ + test_uint32(&ctx, &parse_section_tweak, "max-shm-pool-size-mb", + &conf.tweak.max_shm_pool_size); +#endif + + config_free(&conf); +} + +int +main(int argc, const char *const *argv) +{ + FcInit(); + log_init(LOG_COLORIZE_AUTO, false, 0, LOG_CLASS_ERROR); + test_section_main(); + test_section_security(); + test_section_bell(); + test_section_desktop_notifications(); + test_section_scrollback(); + test_section_url(); + test_section_cursor(); + test_section_mouse(); + test_section_touch(); + test_section_colors_dark(); + test_section_colors_light(); + test_section_csd(); + test_section_key_bindings(); + test_section_key_bindings_collisions(); + test_section_search_bindings(); + test_section_search_bindings_collisions(); + test_section_url_bindings(); + test_section_url_bindings_collisions(); + test_section_mouse_bindings(); + test_section_mouse_bindings_collisions(); + test_section_text_bindings(); + test_section_environment(); + test_section_tweak(); + log_deinit(); + FcFini(); + return 0; +} diff --git a/themes/aeroroot b/themes/aeroroot new file mode 100644 index 0000000..dbeb2e8 --- /dev/null +++ b/themes/aeroroot @@ -0,0 +1,34 @@ +# -*- conf -*- +# Aero root theme + +[colors-dark] +cursor=1a1a1a 9fd5f5 +foreground=dedeef +background=1a1a1a + +regular0=1a1a1a +regular1=ff3a3a +regular2=3aef3a +regular3=e6e61a +regular4=1a7eff +regular5=df3adf +regular6=3ff0e0 +regular7=dadada + +bright0=5a5a5a +bright1=ffaaaa +bright2=aaf3aa +bright3=f3f35a +bright4=6abaff +bright5=e5aae5 +bright6=aafff0 +bright7=f3f3f3 + +dim0=000000 +dim1=b71a1a +dim2=1ab71a +dim3=b5b50a +dim4=0A4FAA +dim5=a71aa7 +dim6=1AA59F +dim7=a5a5a5 diff --git a/themes/alacritty b/themes/alacritty new file mode 100644 index 0000000..f05683b --- /dev/null +++ b/themes/alacritty @@ -0,0 +1,57 @@ +# -*- conf -*- +# Alacritty + +[colors-dark] +cursor = 181818 d8d8d8 +background= 181818 +foreground= d8d8d8 + +#black +regular0= 181818 + +#red +regular1= ac4242 + +#green +regular2= 90a959 + +#yellow +regular3= f4bf75 + +#blue +regular4= 6a9fb5 + +#magenta +regular5= aa759f + +#cyan +regular6= 75b5aa + +#white/grey +regular7= d8d8d8 + + + +#grey/black +bright0= 6b6b6b + +#red +bright1= c55555 + +#green +bright2= aac474 + +#yellow +bright3= feca88 + +#blue +bright4= 82b8c8 + +#pink +bright5= c28cb8 + +#cyan +bright6= 93d3c3 + +#grey +bright7= f8f8f8 \ No newline at end of file diff --git a/themes/apprentice b/themes/apprentice new file mode 100644 index 0000000..291ab8d --- /dev/null +++ b/themes/apprentice @@ -0,0 +1,25 @@ +# -*- conf -*- +# https://github.com/romainl/Apprentice + +[colors-dark] +cursor=262626 6c6c6c +foreground=bcbcbc +background=262626 +regular0=1c1c1c +regular1=af5f5f +regular2=5f875f +regular3=87875f +regular4=5f87af +regular5=5f5f87 +regular6=5f8787 +regular7=6c6c6c +bright0=444444 +bright1=ff8700 +bright2=87af87 +bright3=ffffaf +bright4=87afd7 +bright5=8787af +bright6=5fafaf +bright7=ffffff +# selection-foreground=bcbcbc +# selection-background=3a3e4e \ No newline at end of file diff --git a/themes/ayu-mirage b/themes/ayu-mirage new file mode 100644 index 0000000..2d9b6b5 --- /dev/null +++ b/themes/ayu-mirage @@ -0,0 +1,26 @@ +# -*- conf -*- +# theme: Ayu Mirage +# description: a theme based on Ayu Mirage for Sublime Text (original: https://github.com/dempfi/ayu) + +[colors-dark] +cursor = ffcc66 665a44 +foreground = cccac2 +background = 242936 + +regular0 = 242936 # black +regular1 = f28779 # red +regular2 = d5ff80 # green +regular3 = ffd173 # yellow +regular4 = 73d0ff # blue +regular5 = dfbfff # magenta +regular6 = 5ccfe6 # cyan +regular7 = cccac2 # white + +bright0 = fcfcfc # bright black +bright1 = f07171 # bright red +bright2 = 86b300 # bright gree +bright3 = f2ae49 # bright yellow +bright4 = 399ee6 # bright blue +bright5 = a37acc # bright magenta +bright6 = 55b4d4 # bright cyan +bright7 = 5c6166 # bright white diff --git a/themes/catppuccin-frappe b/themes/catppuccin-frappe new file mode 100644 index 0000000..3acae60 --- /dev/null +++ b/themes/catppuccin-frappe @@ -0,0 +1,38 @@ +# _*_ conf _*_ +# Catppuccin Frappe + +[colors-dark] +foreground=c6d0f5 +background=303446 + +regular0=51576d +regular1=e78284 +regular2=a6d189 +regular3=e5c890 +regular4=8caaee +regular5=f4b8e4 +regular6=81c8be +regular7=b5bfe2 + +bright0=626880 +bright1=e78284 +bright2=a6d189 +bright3=e5c890 +bright4=8caaee +bright5=f4b8e4 +bright6=81c8be +bright7=a5adce + +cursor=232634 f2d5cf + +16=ef9f76 +17=f2d5cf + +selection-foreground=c6d0f5 +selection-background=4f5369 + +search-box-no-match=232634 e78284 +search-box-match=c6d0f5 414559 + +jump-labels=232634 ef9f76 +urls=8caaee diff --git a/themes/catppuccin-latte b/themes/catppuccin-latte new file mode 100644 index 0000000..ca7a7aa --- /dev/null +++ b/themes/catppuccin-latte @@ -0,0 +1,41 @@ +# _*_ conf _*_ +# Catppuccin Latte + +[main] +initial-color-theme=light + +[colors-light] +foreground=4c4f69 +background=eff1f5 + +regular0=5c5f77 +regular1=d20f39 +regular2=40a02b +regular3=df8e1d +regular4=1e66f5 +regular5=ea76cb +regular6=179299 +regular7=acb0be + +bright0=6c6f85 +bright1=d20f39 +bright2=40a02b +bright3=df8e1d +bright4=1e66f5 +bright5=ea76cb +bright6=179299 +bright7=bcc0cc + +cursor=eff1f5 dc8a78 + +16=fe640b +17=dc8a78 + +selection-foreground=4c4f69 +selection-background=ccced7 + +search-box-no-match=dce0e8 d20f39 +search-box-match=4c4f69 ccd0da + +jump-labels=dce0e8 fe640b +urls=1e66f5 diff --git a/themes/catppuccin-macchiato b/themes/catppuccin-macchiato new file mode 100644 index 0000000..8f5ea36 --- /dev/null +++ b/themes/catppuccin-macchiato @@ -0,0 +1,38 @@ +# _*_ conf _*_ +# Catppuccin Macchiato + +[colors-dark] +foreground=cad3f5 +background=24273a + +regular0=494d64 +regular1=ed8796 +regular2=a6da95 +regular3=eed49f +regular4=8aadf4 +regular5=f5bde6 +regular6=8bd5ca +regular7=b8c0e0 + +bright0=5b6078 +bright1=ed8796 +bright2=a6da95 +bright3=eed49f +bright4=8aadf4 +bright5=f5bde6 +bright6=8bd5ca +bright7=a5adcb + +cursor=181926 f4dbd6 + +16=f5a97f +17=f4dbd6 + +selection-foreground=cad3f5 +selection-background=454a5f + +search-box-no-match=181926 ed8796 +search-box-match=cad3f5 363a4f + +jump-labels=181926 f5a97f +urls=8aadf4 diff --git a/themes/catppuccin-mocha b/themes/catppuccin-mocha new file mode 100644 index 0000000..7d98dc0 --- /dev/null +++ b/themes/catppuccin-mocha @@ -0,0 +1,38 @@ +# _*_ conf _*_ +# Catppuccin Mocha + +[colors-dark] +foreground=cdd6f4 +background=1e1e2e + +regular0=45475a +regular1=f38ba8 +regular2=a6e3a1 +regular3=f9e2af +regular4=89b4fa +regular5=f5c2e7 +regular6=94e2d5 +regular7=bac2de + +bright0=585b70 +bright1=f38ba8 +bright2=a6e3a1 +bright3=f9e2af +bright4=89b4fa +bright5=f5c2e7 +bright6=94e2d5 +bright7=a6adc8 + +cursor=11111b f5e0dc + +16=fab387 +17=f5e0dc + +selection-foreground=cdd6f4 +selection-background=414356 + +search-box-no-match=11111b f38ba8 +search-box-match=cdd6f4 313244 + +jump-labels=11111b fab387 +urls=89b4fa diff --git a/themes/chiba-dark b/themes/chiba-dark new file mode 100644 index 0000000..ffaf6cb --- /dev/null +++ b/themes/chiba-dark @@ -0,0 +1,25 @@ +# -*- conf -*- +# theme: Chiba Dark +# author: ayushnix (https://sr.ht/~ayushnix) +# description: A dark theme with bright cyberpunk colors (WCAG AAA compliant) + +[colors-dark] +cursor = 181818 cdcdcd +foreground = cdcdcd +background = 181818 +regular0 = 181818 +regular1 = ff8599 +regular2 = 00c545 +regular3 = de9d00 +regular4 = 00b4ff +regular5 = fd71f8 +regular6 = 00bfae +regular7 = cdcdcd +bright0 = 262626 +bright1 = ff9eb2 +bright2 = 19de5e +bright3 = f7b619 +bright4 = 19cdff +bright5 = ff8aff +bright6 = 19d8c7 +bright7 = dadada diff --git a/themes/derp b/themes/derp new file mode 100644 index 0000000..42af337 --- /dev/null +++ b/themes/derp @@ -0,0 +1,23 @@ +# -*- conf -*- +# Derp + +[colors-dark] +cursor=000000 ffffff +foreground=ffffff +background=000000 +regular0=111111 +regular1=d36265 +regular2=aece91 +regular3=e7e18c +regular4=5297cf +regular5=963c59 +regular6=5e7175 +regular7=bebebe +bright0=666666 +bright1=ef8171 +bright2=cfefb3 +bright3=fff796 +bright4=74b8ef +bright5=b85e7b +bright6=a3babf +bright7=ffffff diff --git a/themes/deus b/themes/deus new file mode 100644 index 0000000..69c4494 --- /dev/null +++ b/themes/deus @@ -0,0 +1,29 @@ +# -*- conf -*- +# Deus +# Color palette based on: https://github.com/ajmwagar/vim-deus + +[colors-dark] +cursor=2c323b eaeaea +background=2c323b +foreground=eaeaea +regular0=242a32 +regular1=d54e53 +regular2=98c379 +regular3=e5c07b +regular4=83a598 +regular5=c678dd +regular6=70c0ba +regular7=eaeaea +bright0=666666 +bright1=ec3e45 +bright2=90c966 +bright3=edbf69 +bright4=73ba9f +bright5=c858e9 +bright6=2bcec2 +bright7=ffffff + +# Enable if prefer Deus colors instead of inverterd fg/bg for +# highlighting (mouse selection) +# selection-foreground=2c323b +# selection-background=eaeaea diff --git a/themes/dracula b/themes/dracula new file mode 100644 index 0000000..8299420 --- /dev/null +++ b/themes/dracula @@ -0,0 +1,23 @@ +# -*- conf -*- +# Dracula + +[colors-dark] +cursor=282a36 f8f8f2 +foreground=f8f8f2 +background=282a36 +regular0=000000 # black +regular1=ff5555 # red +regular2=50fa7b # green +regular3=f1fa8c # yellow +regular4=bd93f9 # blue +regular5=ff79c6 # magenta +regular6=8be9fd # cyan +regular7=bfbfbf # white +bright0=4d4d4d # bright black +bright1=ff6e67 # bright red +bright2=5af78e # bright green +bright3=f4f99d # bright yellow +bright4=caa9fa # bright blue +bright5=ff92d0 # bright magenta +bright6=9aedfe # bright cyan +bright7=e6e6e6 # bright white \ No newline at end of file diff --git a/themes/dracula-iterm b/themes/dracula-iterm new file mode 100644 index 0000000..b75ddd9 --- /dev/null +++ b/themes/dracula-iterm @@ -0,0 +1,23 @@ +# -*- conf -*- +# Dracula iTerm2 variant + +[colors-dark] +cursor=ffffff bbbbbb +foreground=f8f8f2 +background=1e1f29 +regular0=000000 # black +regular1=ff5555 # red +regular2=50fa7b # green +regular3=f1fa8c # yellow +regular4=bd93f9 # blue +regular5=ff79c6 # magenta +regular6=8be9fd # cyan +regular7=bbbbbb # white +bright0=555555 # bright black +bright1=ff5555 # bright red +bright2=50fa7b # bright green +bright3=f1fa8c # bright yellow +bright4=bd93f9 # bright blue +bright5=ff79c6 # bright magenta +bright6=8be9fd # bright cyan +bright7=ffffff # bright white diff --git a/themes/electrophoretic b/themes/electrophoretic new file mode 100644 index 0000000..8bc022e --- /dev/null +++ b/themes/electrophoretic @@ -0,0 +1,36 @@ +# -*- conf -*- +# Electrophoretic +# Theme for electrophoretic displays (like e-ink) which usually supports +# 16 levels of grays. This theme aims to maximize the contrast between the +# text and the white background. +# author: Eugen Rahaian + +[main] +initial-color-theme=light + +[colors-light] +cursor=ffffff 515151 +background= ffffff +foreground= 000000 + +# The colors are sorted based on their luminance, so we can more easily assign +# them a gray level. +# grayscale order: black_0 blue_4 red_1 magenta_5 green_2 cyan_6 yellow_3 white_7 +regular0= ffffff +regular4= 616161 +regular1= 515151 +regular5= 414141 +regular2= 313131 +regular6= 212121 +regular3= 111111 +regular7= 000000 +# Here, we also stay away from the white background by reusing the dark gray levels +# from above, with small variations +bright0= 818181 +bright4= 717171 +bright1= 616161 +bright5= 515151 +bright2= 414141 +bright6= 313131 +bright3= 212121 +bright7= 111111 diff --git a/themes/gruvbox b/themes/gruvbox new file mode 100644 index 0000000..e44f3ea --- /dev/null +++ b/themes/gruvbox @@ -0,0 +1,42 @@ +# -*- conf -*- +# Gruvbox + +[colors-dark] +background=282828 +foreground=ebdbb2 +regular0=282828 +regular1=cc241d +regular2=98971a +regular3=d79921 +regular4=458588 +regular5=b16286 +regular6=689d6a +regular7=a89984 +bright0=928374 +bright1=fb4934 +bright2=b8bb26 +bright3=fabd2f +bright4=83a598 +bright5=d3869b +bright6=8ec07c +bright7=ebdbb2 + +[colors-light] +background=fbf1c7 +foreground=3c3836 +regular0=fbf1c7 +regular1=cc241d +regular2=98971a +regular3=d79921 +regular4=458588 +regular5=b16286 +regular6=689d6a +regular7=7c6f64 +bright0=928374 +bright1=9d0006 +bright2=79740e +bright3=b57614 +bright4=076678 +bright5=8f3f71 +bright6=427b58 +bright7=3c3836 diff --git a/themes/gruvbox-dark b/themes/gruvbox-dark new file mode 100644 index 0000000..c5dadcc --- /dev/null +++ b/themes/gruvbox-dark @@ -0,0 +1,22 @@ +# -*- conf -*- +# Gruvbox + +[colors-dark] +background=282828 +foreground=ebdbb2 +regular0=282828 +regular1=cc241d +regular2=98971a +regular3=d79921 +regular4=458588 +regular5=b16286 +regular6=689d6a +regular7=a89984 +bright0=928374 +bright1=fb4934 +bright2=b8bb26 +bright3=fabd2f +bright4=83a598 +bright5=d3869b +bright6=8ec07c +bright7=ebdbb2 diff --git a/themes/gruvbox-light b/themes/gruvbox-light new file mode 100644 index 0000000..6b61661 --- /dev/null +++ b/themes/gruvbox-light @@ -0,0 +1,25 @@ +# -*- conf -*- +# Gruvbox - Light + +[main] +initial-color-theme=light + +[colors-light] +background=fbf1c7 +foreground=3c3836 +regular0=fbf1c7 +regular1=cc241d +regular2=98971a +regular3=d79921 +regular4=458588 +regular5=b16286 +regular6=689d6a +regular7=7c6f64 +bright0=928374 +bright1=9d0006 +bright2=79740e +bright3=b57614 +bright4=076678 +bright5=8f3f71 +bright6=427b58 +bright7=3c3836 diff --git a/themes/hacktober b/themes/hacktober new file mode 100644 index 0000000..ecdb18f --- /dev/null +++ b/themes/hacktober @@ -0,0 +1,22 @@ +# -*- conf -*- + +[colors-dark] +cursor=141414 c9c9c9 +foreground=c9c9c9 +background=141414 +regular0=191918 # black +regular1=b34538 # red +regular2=587744 # green +regular3=d08949 # yellow +regular4=206ec5 # blue +regular5=864651 # magenta +regular6=ac9166 # cyan +regular7=f1eee7 # white +bright0=2c2b2a # bright black +bright1=b33323 # bright red +bright2=42824a # bright green +bright3=c75a22 # bright yellow +bright4=5389c5 # bright blue +bright5=e795a5 # bright magenta +bright6=ebc587 # bright cyan +bright7=ffffff # bright white diff --git a/themes/iterm b/themes/iterm new file mode 100644 index 0000000..c5ffc19 --- /dev/null +++ b/themes/iterm @@ -0,0 +1,27 @@ +# -*- conf -*- +# this foot theme is based on alacritty iterm theme: +# https://github.com/alacritty/alacritty-theme/blob/master/themes/iterm.toml + +[colors-dark] +foreground=fffbf6 +background=101421 + +## Normal/regular colors (color palette 0-7) +regular0=2e2e2e # black +regular1=eb4129 # red +regular2=abe047 # green +regular3=f6c744 # yellow +regular4=47a0f3 # blue +regular5=7b5cb0 # magenta +regular6=64dbed # cyan +regular7=e5e9f0 # white + +## Bright colors (color palette 8-15) +bright0=565656 # bright black +bright1=ec5357 # bright red +bright2=c0e17d # bright green +bright3=f9da6a # bright yellow +bright4=49a4f8 # bright blue +bright5=a47de9 # bright magenta +bright6=99faf2 # bright cyan +bright7=ffffff # bright white diff --git a/themes/jetbrains-darcula b/themes/jetbrains-darcula new file mode 100644 index 0000000..0092b79 --- /dev/null +++ b/themes/jetbrains-darcula @@ -0,0 +1,26 @@ +# -*- conf -*- +# JetBrains Darcula +# Palette based on the same theme from https://github.com/dexpota/kitty-themes + +[colors-dark] +cursor=202020 ffffff +background=202020 +foreground=adadad +regular0=000000 # black +regular1=fa5355 # red +regular2=126e00 # green +regular3=c2c300 # yellow +regular4=4581eb # blue +regular5=fa54ff # magenta +regular6=33c2c1 # cyan +regular7=adadad # white +bright0=545454 # bright black +bright1=fb7172 # bright red +bright2=67ff4f # bright green +bright3=ffff00 # bright yellow +bright4=6d9df1 # bright blue +bright5=fb82ff # bright magenta +bright6=60d3d1 # bright cyan +bright7=eeeeee # bright white +# selection-foreground=202020 +# selection-background=1a3272 diff --git a/themes/kitty b/themes/kitty new file mode 100644 index 0000000..81fd003 --- /dev/null +++ b/themes/kitty @@ -0,0 +1,22 @@ +# -*- conf -*- + +[colors-dark] +cursor=111111 cccccc +foreground=dddddd +background=000000 +regular0=000000 # black +regular1=cc0403 # red +regular2=19cb00 # green +regular3=cecb00 # yellow +regular4=0d73cc # blue +regular5=cb1ed1 # magenta +regular6=0dcdcd # cyan +regular7=dddddd # white +bright0=767676 # bright black +bright1=f2201f # bright red +bright2=23fd00 # bright green +bright3=fffd00 # bright yellow +bright4=1a8fff # bright blue +bright5=fd28ff # bright magenta +bright6=14ffff # bright cyan +bright7=ffffff # bright white diff --git a/themes/material-amber b/themes/material-amber new file mode 100644 index 0000000..69126aa --- /dev/null +++ b/themes/material-amber @@ -0,0 +1,41 @@ +# -*- conf -*- +# Material Amber +# Based on material.io guidelines with Amber 50 background + +[main] +initial-color-theme=light + +[colors-light] +cursor=fff8e1 21201d +foreground = 21201d +background = fff8e1 + +regular0 = 21201d # black +regular1 = cd4340 # red +regular2 = 498d49 # green +regular3 = fab32d # yellow +regular4 = 3378c4 # blue +regular5 = b83269 # magenta +regular6 = 21929a # cyan +regular7 = ffd7d7 # white + +bright0 = 66635a # bright black +bright1 = dd7b72 # bright red +bright2 = 82ae78 # bright green +bright3 = fbc870 # bright yellow +bright4 = 73a0cd # bright blue +bright5 = ce6f8e # bright magenta +bright6 = 548c94 # bright cyan +bright7 = ffe1da # bright white + +dim0 = 9e9a8c # dim black +dim1 = e9a99b # dim red +dim2 = b0c99f # dim green +dim3 = fdda9a # dim yellow +dim4 = a6c0d4 # dim blue +dim5 = e0a1ad # dim magenta +dim6 = 3c6064 # dim cyan +dim7 = ffe9dd # dim white + +# selection-foreground=fff8e1 +# selection-background=21201d diff --git a/themes/material-design b/themes/material-design new file mode 100644 index 0000000..bf1d0a6 --- /dev/null +++ b/themes/material-design @@ -0,0 +1,25 @@ +# -*- conf -*- +# Material +# From https://github.com/MartinSeeler/iterm2-material-design + +[colors-dark] +foreground=ECEFF1 +background=263238 +regular0=546E7A # black +regular1=FF5252 # red +regular2=5CF19E # green +regular3=FFD740 # yellow +regular4=40C4FF # blue +regular5=FF4081 # magenta +regular6=64FCDA # cyan +regular7=FFFFFF # white +bright0=B0BEC5 # bright black +bright1=FF8A80 # bright red +bright2=B9F6CA # bright green +bright3=FFE57F # bright yellow +bright4=80D8FF # bright blue +bright5=FF80AB # bright magenta +bright6=A7FDEB # bright cyan +bright7=FFFFFF # bright white +# selection-foreground=ECEFF1 +# selection-background=607D8B diff --git a/themes/modus-operandi b/themes/modus-operandi new file mode 100644 index 0000000..6baca2f --- /dev/null +++ b/themes/modus-operandi @@ -0,0 +1,30 @@ +# -*- conf -*- +# +# modus-operandi +# See: https://protesilaos.com/emacs/modus-themes +# + +[main] +initial-color-theme=light + +[colors-light] +background=ffffff +foreground=000000 +regular0=000000 +regular1=a60000 +regular2=005e00 +regular3=813e00 +regular4=0031a9 +regular5=721045 +regular6=00538b +regular7=bfbfbf +bright0=595959 +bright1=972500 +bright2=315b00 +bright3=70480f +bright4=2544bb +bright5=5317ac +bright6=005a5f +bright7=ffffff + +jump-labels=dce0e8 0000ff diff --git a/themes/modus-vivendi b/themes/modus-vivendi new file mode 100644 index 0000000..9ee670e --- /dev/null +++ b/themes/modus-vivendi @@ -0,0 +1,25 @@ +# -*- conf -*- +# +# modus-vivendi +# See: https://protesilaos.com/emacs/modus-themes +# + +[colors-dark] +background=000000 +foreground=ffffff +regular0=000000 +regular1=ff8059 +regular2=44bc44 +regular3=d0bc00 +regular4=2fafff +regular5=feacd0 +regular6=00d3d0 +regular7=bfbfbf +bright0=595959 +bright1=ef8b50 +bright2=70b900 +bright3=c0c530 +bright4=79a8ff +bright5=b6a0ff +bright6=6ae4b9 +bright7=ffffff diff --git a/themes/modus-vivendi-tinted b/themes/modus-vivendi-tinted new file mode 100644 index 0000000..6a61fc7 --- /dev/null +++ b/themes/modus-vivendi-tinted @@ -0,0 +1,25 @@ +# -*- conf -*- +# +# modus-vivendi-tinted +# See: https://protesilaos.com/emacs/modus-themes +# + +[colors-dark] +background=0d0e1c +foreground=ffffff +regular0=000000 +regular1=ff5f59 +regular2=44bc44 +regular3=d0bc00 +regular4=2fafff +regular5=feacd0 +regular6=00d3d0 +regular7=a6a6a6 +bright0=595959 +bright1=ff6b55 +bright2=ff6b55 +bright3=fec43f +bright4=fec43f +bright5=b6a0ff +bright6=6ae4b9 +bright7=777777 diff --git a/themes/molokai b/themes/molokai new file mode 100644 index 0000000..19e1b6f --- /dev/null +++ b/themes/molokai @@ -0,0 +1,23 @@ +# -*- conf -*- +# Molokai +# Based on zhou13's at https://github.com/zhou13/molokai-terminal/blob/master/xterm/Xresources + +[colors-dark] +background=1B1D1E +foreground=CCCCCC +regular0=1B1D1E +regular1=FF0044 +regular2=82B414 +regular3=FD971F +regular4=266C98 +regular5=AC0CB1 +regular6=AE81FF +regular7=CCCCCC +bright0=808080 +bright1=F92672 +bright2=A6E22E +bright3=E6DB74 +bright4=7070F0 +bright5=D63AE1 +bright6=66D9EF +bright7=F8F8F2 diff --git a/themes/monokai-pro b/themes/monokai-pro new file mode 100644 index 0000000..3044da9 --- /dev/null +++ b/themes/monokai-pro @@ -0,0 +1,22 @@ +# -*- conf -*- +# Monokai Pro + +[colors-dark] +background=2D2A2E +foreground=FCFCFA +regular0=403E41 +regular1=FF6188 +regular2=A9DC76 +regular3=FFD866 +regular4=FC9867 +regular5=AB9DF2 +regular6=78DCE8 +regular7=FCFCFA +bright0=727072 +bright1=FF6188 +bright2=A9DC76 +bright3=FFD866 +bright4=FC9867 +bright5=AB9DF2 +bright6=78DCE8 +bright7=FCFCFA diff --git a/themes/moonfly b/themes/moonfly new file mode 100644 index 0000000..b30e315 --- /dev/null +++ b/themes/moonfly @@ -0,0 +1,29 @@ +# -*- conf -*- +# moonfly +# Based on https://github.com/bluz71/vim-moonfly-colors + +[colors-dark] +cursor = 080808 9e9e9e +foreground = b2b2b2 +background = 080808 + +# selection-foreground = 080808 +# selection-background = b2ceee + +regular0 = 323437 +regular1 = ff5454 +regular2 = 8cc85f +regular3 = e3c78a +regular4 = 80a0ff +regular5 = d183e8 +regular6 = 79dac8 +regular7 = c6c6c6 + +bright0 = 949494 +bright1 = ff5189 +bright2 = 36c692 +bright3 = c2c292 +bright4 = 74b2ff +bright5 = ae81ff +bright6 = 85dc85 +bright7 = e4e4e4 diff --git a/themes/neon b/themes/neon new file mode 100644 index 0000000..74884e0 --- /dev/null +++ b/themes/neon @@ -0,0 +1,27 @@ +# +# vim: ft=dosini +# +# Neon +# +# https://xcolors.net/neon +# + +[colors-dark] +foreground=f8f8f8 +background=171717 +regular0=171717 +regular1=d81765 +regular2=97d01a +regular3=ffa800 +regular4=16b1fb +regular5=ff2491 +regular6=0fdcb6 +regular7=ebebeb +bright0=38252c +bright1=ff0000 +bright2=76b639 +bright3=e1a126 +bright4=289cd5 +bright5=ff2491 +bright6=0a9b81 +bright7=f8f8f8 diff --git a/themes/night-owl b/themes/night-owl new file mode 100644 index 0000000..e9e4040 --- /dev/null +++ b/themes/night-owl @@ -0,0 +1,28 @@ +# _*_ conf _*_ +# Night Owl + +[colors-dark] +cursor=011627 80a4c2 +foreground=d6deeb +background=011627 + +regular0=011627 +regular1=ef5350 +regular2=22da6e +regular3=addb67 +regular4=82aaff +regular5=c792ea +regular6=21c7a8 +regular7=ffffff + +bright0=575656 +bright1=ef5350 +bright2=22da6e +bright3=ffeb95 +bright4=82aaff +bright5=c792ea +bright6=7fdbca +bright7=ffffff + +selection-background=5f7e97 +selection-foreground=dfe5ee diff --git a/themes/nightfly b/themes/nightfly new file mode 100644 index 0000000..ccdd183 --- /dev/null +++ b/themes/nightfly @@ -0,0 +1,29 @@ +# -*- conf -*- +# nightfly +# Based on https://github.com/bluz71/vim-nightfly-guicolors + +[colors-dark] +cursor = 080808 9ca1aa +foreground = acb4c2 +background = 011627 + +# selection-foreground = 080808 +# selection-background = b2ceee + +regular0 = 1d3b53 +regular1 = fc514e +regular2 = a1cd5e +regular3 = e3d18a +regular4 = 82aaff +regular5 = c792ea +regular6 = 7fdbca +regular7 = a1aab8 + +bright0 = 7c8f8f +bright1 = ff5874 +bright2 = 21c7a8 +bright3 = ecc48d +bright4 = 82aaff +bright5 = ae81ff +bright6 = ae81ff +bright7 = d6deeb diff --git a/themes/noirblaze b/themes/noirblaze new file mode 100644 index 0000000..b21055a --- /dev/null +++ b/themes/noirblaze @@ -0,0 +1,29 @@ +# -*- conf -*- +# noirblaze-kitty +# https://github.com/n1ghtmare/noirblaze-kitty + + +[colors-dark] +cursor=121212 ff0088 +foreground=d5d5d5 +background=121212 + +# selection-foreground=121212 +# selection-background=b0b0b0 + +regular0=121212 # black +regular1=ff0088 # red +regular2=00ff77 # green +regular3=ffffff # yellow +regular4=b0b0b0 # blue +regular5=7a7a7a # magenta +regular6=787878 # cyan +regular7=d5d5d5 # white +bright0=737373 # bright black +bright1=FD319E # bright red +bright2=FD319E # bright green +bright3=FDFDFD # bright yellow +bright4=BEBEBE # bright blue +bright5=939393 # bright magenta +bright6=919191 # bright cyan +bright7=f5f5f5 # bright white diff --git a/themes/nord b/themes/nord new file mode 100644 index 0000000..eb2fdf0 --- /dev/null +++ b/themes/nord @@ -0,0 +1,42 @@ +# -*- conf -*- +# theme: Nord +# author: Arctic Ice Studio , Sven Greb +# description: „Nord“ — An arctic, north-bluish color palette +# +# this specific foot theme is based on nord-alacritty: +# https://github.com/arcticicestudio/nord-alacritty/blob/develop/src/nord.yml + +[colors-dark] +cursor = 2e3440 d8dee9 +foreground = d8dee9 +background = 2e3440 + +# selection-foreground = d8dee9 +# selection-background = 4c566a + +regular0 = 3b4252 +regular1 = bf616a +regular2 = a3be8c +regular3 = ebcb8b +regular4 = 81a1c1 +regular5 = b48ead +regular6 = 88c0d0 +regular7 = e5e9f0 + +bright0 = 4c566a +bright1 = bf616a +bright2 = a3be8c +bright3 = ebcb8b +bright4 = 81a1c1 +bright5 = b48ead +bright6 = 8fbcbb +bright7 = eceff4 + +dim0 = 373e4d +dim1 = 94545d +dim2 = 809575 +dim3 = b29e75 +dim4 = 68809a +dim5 = 8c738c +dim6 = 6d96a5 +dim7 = aeb3bb diff --git a/themes/nordiq b/themes/nordiq new file mode 100644 index 0000000..1efccba --- /dev/null +++ b/themes/nordiq @@ -0,0 +1,24 @@ +# -*- conf -*- +# Nordiq + +[colors-dark] +cursor=eeeeee 9f515a +foreground=dbdee9 +background=0e1420 +regular0=5b6272 +regular1=bf616a +regular2=a3be8c +regular3=ebcb8b +regular4=81a1c1 +regular5=b48ead +regular6=88c0d0 +regular7=e5e9f0 +bright0=4c566a +bright1=bf616a +bright2=a3be8c +bright3=ebcb8b +bright4=81a1c1 +bright5=b48ead +bright6=8fbcbb +bright7=eceff4 + diff --git a/themes/nvim b/themes/nvim new file mode 100644 index 0000000..74dd1ac --- /dev/null +++ b/themes/nvim @@ -0,0 +1,56 @@ +# -*- conf -*- +# Neovim Dark theme +# Uses the dark color palette from the default Neovim color scheme +# See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L419 + +[colors-dark] +cursor=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2 +foreground=e0e2ea # NvimLightGrey2 +background=14161b # NvimDarkGrey2 + +selection-foreground=e0e2ea # NvimLightGrey2 +selection-background=4f5258 # NvimDarkGrey4 + +regular0=07080d # NvimDarkGrey1 +regular1=ffc0b9 # NvimLightRed +regular2=b3f6c0 # NvimLightGreen +regular3=fce094 # NvimLightYellow +regular4=a6dbff # NvimLightBlue +regular5=ffcaff # NvimLightMagenta +regular6=8cf8f7 # NvimLightCyan +regular7=c4c6cd # NvimLightGrey3 + +bright0=2c2e33 # NvimDarkGrey3 +bright1=ffc0b9 # NvimLightRed +bright2=b3f6c0 # NvimLightGreen +bright3=fce094 # NvimLightYellow +bright4=a6dbff # NvimLightBlue +bright5=ffcaff # NvimLightMagenta +bright6=8cf8f7 # NvimLightCyan +bright7=eef1f8 # NvimLightGrey1 + +[colors-light] +cursor=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2 +foreground=14161b # NvimDarkGrey2 +background=e0e2ea # NvimLightGrey2 + +selection-foreground=14161b # NvimDarkGrey2 +selection-background=9b9ea4 # NvimLightGrey4 + +regular0=eef1f8 # NvimLightGrey1 +regular1=590008 # NvimDarkRed +regular2=005523 # NvimDarkGreen +regular3=6b5300 # NvimDarkYellow +regular4=004c73 # NvimDarkBlue +regular5=470045 # NvimDarkMagenta +regular6=007373 # NvimDarkCyan +regular7=2c2e33 # NvimDarkGrey3 + +bright0=c4c6cd # NvimLightGrey3 +bright1=590008 # NvimDarkRed +bright2=005523 # NvimDarkGreen +bright3=6b5300 # NvimDarkYellow +bright4=004c73 # NvimDarkBlue +bright5=470045 # NvimDarkMagenta +bright6=007373 # NvimDarkCyan +bright7=07080d # NvimDarkGrey1 diff --git a/themes/nvim-dark b/themes/nvim-dark new file mode 100644 index 0000000..fe3afb7 --- /dev/null +++ b/themes/nvim-dark @@ -0,0 +1,30 @@ +# -*- conf -*- +# Neovim Dark theme +# Uses the dark color palette from the default Neovim color scheme +# See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L419 + +[colors-dark] +cursor=14161b e0e2ea # NvimDarkGrey2 NvimLightGrey2 +foreground=e0e2ea # NvimLightGrey2 +background=14161b # NvimDarkGrey2 + +selection-foreground=e0e2ea # NvimLightGrey2 +selection-background=4f5258 # NvimDarkGrey4 + +regular0=07080d # NvimDarkGrey1 +regular1=ffc0b9 # NvimLightRed +regular2=b3f6c0 # NvimLightGreen +regular3=fce094 # NvimLightYellow +regular4=a6dbff # NvimLightBlue +regular5=ffcaff # NvimLightMagenta +regular6=8cf8f7 # NvimLightCyan +regular7=c4c6cd # NvimLightGrey3 + +bright0=2c2e33 # NvimDarkGrey3 +bright1=ffc0b9 # NvimLightRed +bright2=b3f6c0 # NvimLightGreen +bright3=fce094 # NvimLightYellow +bright4=a6dbff # NvimLightBlue +bright5=ffcaff # NvimLightMagenta +bright6=8cf8f7 # NvimLightCyan +bright7=eef1f8 # NvimLightGrey1 diff --git a/themes/nvim-light b/themes/nvim-light new file mode 100644 index 0000000..fd8943b --- /dev/null +++ b/themes/nvim-light @@ -0,0 +1,33 @@ +# -*- conf -*- +# Neovim Light theme +# Uses the light color palette from the default Neovim color scheme +# See: https://github.com/neovim/neovim/blob/fb6c059dc55c8d594102937be4dd70f5ff51614a/src/nvim/highlight_group.c#L334 + +[main] +initial-color-theme=light + +[colors-light] +cursor=e0e2ea 14161b # NvimLightGrey2 NvimDarkGrey2 +foreground=14161b # NvimDarkGrey2 +background=e0e2ea # NvimLightGrey2 + +selection-foreground=14161b # NvimDarkGrey2 +selection-background=9b9ea4 # NvimLightGrey4 + +regular0=eef1f8 # NvimLightGrey1 +regular1=590008 # NvimDarkRed +regular2=005523 # NvimDarkGreen +regular3=6b5300 # NvimDarkYellow +regular4=004c73 # NvimDarkBlue +regular5=470045 # NvimDarkMagenta +regular6=007373 # NvimDarkCyan +regular7=2c2e33 # NvimDarkGrey3 + +bright0=c4c6cd # NvimLightGrey3 +bright1=590008 # NvimDarkRed +bright2=005523 # NvimDarkGreen +bright3=6b5300 # NvimDarkYellow +bright4=004c73 # NvimDarkBlue +bright5=470045 # NvimDarkMagenta +bright6=007373 # NvimDarkCyan +bright7=07080d # NvimDarkGrey1 diff --git a/themes/onedark b/themes/onedark new file mode 100644 index 0000000..6d66e87 --- /dev/null +++ b/themes/onedark @@ -0,0 +1,25 @@ +# OneDark +# Palette based on the same theme from https://github.com/dexpota/kitty-themes + +[colors-dark] +cursor=111111 cccccc +foreground=979eab +background=282c34 +regular0=282c34 # black +regular1=e06c75 # red +regular2=98c379 # green +regular3=e5c07b # yellow +regular4=61afef # blue +regular5=be5046 # magenta +regular6=56b6c2 # cyan +regular7=979eab # white +bright0=393e48 # bright black +bright1=d19a66 # bright red +bright2=56b6c2 # bright green +bright3=e5c07b # bright yellow +bright4=61afef # bright blue +bright5=be5046 # bright magenta +bright6=56b6c2 # bright cyan +bright7=abb2bf # bright white +# selection-foreground=282c34 +# selection-background=979eab diff --git a/themes/onehalf-dark b/themes/onehalf-dark new file mode 100644 index 0000000..1faca45 --- /dev/null +++ b/themes/onehalf-dark @@ -0,0 +1,37 @@ +# theme: One Half - dark version +# author: Son A. Pham +# repo: https://github.com/sonph/onehalf +# +# foot theme is based mainly on values from this specific file: +# https://github.com/sonph/onehalf/blob/master/kitty/onehalf-dark.conf +# + cursor colors from: +# https://github.com/sonph/onehalf/blob/master/iterm/OneHalfDark.itermcolors + +[colors-dark] +cursor=dcdfe4 a3b3cc +foreground=dcdfe4 +background=282c34 +regular0=282c34 # black +regular1=e06c75 # red +regular2=98c379 # green +regular3=e5c07b # yellow +regular4=61afef # blue +regular5=c678dd # magenta +regular6=56b6c2 # cyan +regular7=dcdfe4 # white +bright0=5d677a # bright black +bright1=e06c75 # bright red +bright2=98c379 # bright green +bright3=e5c07b # bright yellow +bright4=61afef # bright blue +bright5=c678dd # bright magenta +bright6=56b6c2 # bright cyan +bright7=dcdfe4 # bright white + +# Enable if prefer theme color for URL underline +# urls=0087bd + +# Enable if prefer theme colors instead of inverterd fg/bg for +# highlighting (mouse selection) +# selection-foreground=000000 +# selection-background=fffacd diff --git a/themes/panda b/themes/panda new file mode 100644 index 0000000..2c1dc7c --- /dev/null +++ b/themes/panda @@ -0,0 +1,27 @@ +# -*- conf -*- +# http://panda.siamak.me/ + +[colors-dark] +# alpha=1.0 +background=1D1E20 +foreground=F0F0F0 + +## Normal/regular colors (color palette 0-7) +regular0=1F1F20 # black +regular1=FB055A # red +regular2=26FFD4 # green +regular3=26FFD4 # yellow +regular4=5C9FFF # blue +regular5=FC59A6 # magenta +regular6=26FFD4 # cyan +regular7=F0F0F0 # white + +## Bright colors (color palette 8-15) +bright0=5C6370 # bright black +bright1=FB055A # bright red +bright2=26FFD4 # bright green +bright3=FEBE7E # bright yellow +bright4=55ADFF # bright blue +bright5=FD95D0 # bright magenta +bright6=26FFD4 # bright cyan +bright7=F0F0F0 # bright white \ No newline at end of file diff --git a/themes/paper-color b/themes/paper-color new file mode 100644 index 0000000..0993492 --- /dev/null +++ b/themes/paper-color @@ -0,0 +1,49 @@ +# -*- conf -*- +# PaperColorDark +# Palette based on https://github.com/NLKNguyen/papercolor-theme + +[colors-dark] +cursor=1c1c1c eeeeee +background=1c1c1c +foreground=eeeeee +regular0=1c1c1c # black +regular1=af005f # red +regular2=5faf00 # green +regular3=d7af5f # yellow +regular4=5fafd7 # blue +regular5=808080 # magenta +regular6=d7875f # cyan +regular7=d0d0d0 # white +bright0=bcbcbc # bright black +bright1=5faf5f # bright red +bright2=afd700 # bright green +bright3=af87d7 # bright yellow +bright4=ffaf00 # bright blue +bright5=ff5faf # bright magenta +bright6=00afaf # bright cyan +bright7=5f8787 # bright white +# selection-foreground=1c1c1c +# selection-background=af87d7 + +[colors-light] +cursor=eeeeee 444444 +background=eeeeee +foreground=444444 +regular0=eeeeee # black +regular1=af0000 # red +regular2=008700 # green +regular3=5f8700 # yellow +regular4=0087af # blue +regular5=878787 # magenta +regular6=005f87 # cyan +regular7=764e37 # white +bright0=bcbcbc # bright black +bright1=d70000 # bright red +bright2=d70087 # bright green +bright3=8700af # bright yellow +bright4=d75f00 # bright blue +bright5=d75f00 # bright magenta +bright6=4c7a5d # bright cyan +bright7=005faf # bright white +# selection-foreground=eeeeee +# selection-background=0087af diff --git a/themes/paper-color-dark b/themes/paper-color-dark new file mode 100644 index 0000000..26260c6 --- /dev/null +++ b/themes/paper-color-dark @@ -0,0 +1,26 @@ +# -*- conf -*- +# PaperColorDark +# Palette based on https://github.com/NLKNguyen/papercolor-theme + +[colors-dark] +cursor=1c1c1c eeeeee +background=1c1c1c +foreground=eeeeee +regular0=1c1c1c # black +regular1=af005f # red +regular2=5faf00 # green +regular3=d7af5f # yellow +regular4=5fafd7 # blue +regular5=808080 # magenta +regular6=d7875f # cyan +regular7=d0d0d0 # white +bright0=bcbcbc # bright black +bright1=5faf5f # bright red +bright2=afd700 # bright green +bright3=af87d7 # bright yellow +bright4=ffaf00 # bright blue +bright5=ff5faf # bright magenta +bright6=00afaf # bright cyan +bright7=5f8787 # bright white +# selection-foreground=1c1c1c +# selection-background=af87d7 diff --git a/themes/paper-color-light b/themes/paper-color-light new file mode 100644 index 0000000..554aabc --- /dev/null +++ b/themes/paper-color-light @@ -0,0 +1,29 @@ +# -*- conf -*- +# PaperColor Light +# Palette based on https://github.com/NLKNguyen/papercolor-theme + +[main] +initial-color-theme=light + +[colors-light] +cursor=eeeeee 444444 +background=eeeeee +foreground=444444 +regular0=eeeeee # black +regular1=af0000 # red +regular2=008700 # green +regular3=5f8700 # yellow +regular4=0087af # blue +regular5=878787 # magenta +regular6=005f87 # cyan +regular7=764e37 # white +bright0=bcbcbc # bright black +bright1=d70000 # bright red +bright2=d70087 # bright green +bright3=8700af # bright yellow +bright4=d75f00 # bright blue +bright5=d75f00 # bright magenta +bright6=4c7a5d # bright cyan +bright7=005faf # bright white +# selection-foreground=eeeeee +# selection-background=0087af diff --git a/themes/poimandres b/themes/poimandres new file mode 100644 index 0000000..a2123ac --- /dev/null +++ b/themes/poimandres @@ -0,0 +1,28 @@ +# Based on Poimandres color theme for kitti terminal emulator +# https://github.com/ubmit/poimandres-kitty + +[colors-dark] +cursor=1b1e28 ffffff +foreground=a6accd +background=1b1e28 + +regular0=1b1e28 +regular1=d0679d +regular2=5de4c7 +regular3=fffac2 +regular4=89ddff +regular5=fcc5e9 +regular6=add7ff +regular7=ffffff + +bright0=a6accd +bright1=d0679d +bright2=5de4c7 +bright3=fffac2 +bright4=add7ff +bright5=fae4fc +bright6=89ddff +bright7=ffffff + +selection-background=28344a +selection-foreground=a6accd diff --git a/themes/rezza b/themes/rezza new file mode 100644 index 0000000..62a08cc --- /dev/null +++ b/themes/rezza @@ -0,0 +1,38 @@ +# -*- conf -*- +# theme: rezza +# author: Doug Whiteley (rezza) +# original URL: http://metawire.org/~rezza/index.php +# currently available via: https://web.archive.org/web/20060207133656/http://metawire.org/~rezza/index.php?page=Configs +# this palette was also posted here: +# https://bbs.archlinux.org/viewtopic.php?id=17322 +# +# more description: +# original colors are similar to `phrakture` (https://github.com/f4cket/everycolorschemeivefound/blob/master/phrakture) +# Note: there is also a slightly modified (earlier?) version (also called rezza) which can be found here: +# https://sheet.shiar.nl/source/data/termcol-xcolor.inc.pl +# and also posted here: +# https://forums.debian.net/viewtopic.php?t=29981 + +[colors-dark] +foreground = cccccc +background = 191911 + +# Normal colors +regular0 = 222222 +regular1 = 803232 +regular2 = 5b762f +regular3 = aa9943 +regular4 = 324c80 +regular5 = 706c9a +regular6 = 92b19e +regular7 = ffffff + +# Bright colors +bright0 = 222222 +bright1 = 982b2b +bright2 = 89b83f +bright3 = efef60 +bright4 = 2b4f98 +bright5 = 826ab1 +bright6 = a1cdcd +bright7 = dedede diff --git a/themes/rose-pine b/themes/rose-pine new file mode 100644 index 0000000..b9aa7e2 --- /dev/null +++ b/themes/rose-pine @@ -0,0 +1,28 @@ +# -*- conf -*- +# Rosé Pine + +[colors-dark] +cursor=191724 e0def4 +background=191724 +foreground=e0def4 + +regular0=26233a # black (Overlay) +regular1=eb6f92 # red (Love) +regular2=9ccfd8 # green (Foam) +regular3=f6c177 # yellow (Gold) +regular4=31748f # blue (Pine) +regular5=c4a7e7 # magenta (Iris) +regular6=ebbcba # cyan (Rose) +regular7=e0def4 # white (Text) + +bright0=47435d # bright black (lighter Overlay) +bright1=ff98ba # bright red (lighter Love) +bright2=c5f9ff # bright green (lighter Foam) +bright3=ffeb9e # bright yellow (lighter Gold) +bright4=5b9ab7 # bright blue (lighter Pine) +bright5=eed0ff # bright magenta (lighter Iris) +bright6=ffe5e3 # bright cyan (lighter Rose) +bright7=fefcff # bright white (lighter Text) + +flash=f6c177 # yellow (Gold) + diff --git a/themes/rose-pine-dawn b/themes/rose-pine-dawn new file mode 100644 index 0000000..d2742c7 --- /dev/null +++ b/themes/rose-pine-dawn @@ -0,0 +1,32 @@ +# -*- conf -*- +# Rosé Pine Dawn + +[main] +initial-color-theme=light + + +[colors-light] +cursor=faf4ed 575279 +background=faf4ed +foreground=575279 + +regular0=f2e9e1 # black (Overlay) +regular1=b4637a # red (Love) +regular2=56949f # green (Foam) +regular3=ea9d34 # yellow (Gold) +regular4=286983 # blue (Pine) +regular5=907aa9 # magenta (Iris) +regular6=d7827e # cyan (Rose) +regular7=575279 # white (Text) + +bright0=fffdf5 # bright black (lighter Overlay) +bright1=df8aa0 # bright red (lighter Love) +bright2=7ebcc7 # bright green (lighter Foam) +bright3=ffc55c # bright yellow (lighter Gold) +bright4=538faa # bright blue (lighter Pine) +bright5=b8a1d2 # bright magenta (lighter Iris) +bright6=ffaaa5 # bright cyan (lighter Rose) +bright7=7c76a0 # bright white (lighter Text) + +flash=ea9d34 # yellow (Gold) + diff --git a/themes/rose-pine-moon b/themes/rose-pine-moon new file mode 100644 index 0000000..51b9a33 --- /dev/null +++ b/themes/rose-pine-moon @@ -0,0 +1,28 @@ +# -*- conf -*- +# Rosé Pine Moon + +[colors-dark] +cursor=232136 e0def4 +background=232136 +foreground=e0def4 + +regular0=393552 # black (Overlay) +regular1=eb6f92 # red (Love) +regular2=9ccfd8 # green (Foam) +regular3=f6c177 # yellow (Gold) +regular4=3e8fb0 # blue (Pine) +regular5=c4a7e7 # magenta (Iris) +regular6=ea9a97 # cyan (Rose) +regular7=e0def4 # white (Text) + +bright0=5c5776 # bright black (lighter Overlay) +bright1=ff98ba # bright red (lighter Love) +bright2=c5f9ff # bright green (lighter Foam) +bright3=ffeb9e # bright yellow (lighter Gold) +bright4=6ab7d9 # bright blue (lighter Pine) +bright5=eed0ff # bright magenta (lighter Iris) +bright6=ffc3bf # bright cyan (lighter Rose) +bright7=fefcff # bright white (lighter Text) + +flash=f6c177 # yellow (Gold) + diff --git a/themes/selenized b/themes/selenized new file mode 100644 index 0000000..83fea61 --- /dev/null +++ b/themes/selenized @@ -0,0 +1,48 @@ +# -*- conf -*- +# Selenized dark + +[colors-dark] +cursor = 103c48 53d6c7 +background= 103c48 +foreground= adbcbc + +regular0= 184956 +regular1= fa5750 +regular2= 75b938 +regular3= dbb32d +regular4= 4695f7 +regular5= f275be +regular6= 41c7b9 +regular7= 72898f + +bright0= 2d5b69 +bright1= ff665c +bright2= 84c747 +bright3= ebc13d +bright4= 58a3ff +bright5= ff84cd +bright6= 53d6c7 +bright7= cad8d9 + +[colors-light] +cursor=fbf3db 00978a +background= fbf3db +foreground= 53676d + +regular0= ece3cc +regular1= d2212d +regular2= 489100 +regular3= ad8900 +regular4= 0072d4 +regular5= ca4898 +regular6= 009c8f +regular7= 909995 + +bright0= d5cdb6 +bright1= cc1729 +bright2= 428b00 +bright3= a78300 +bright4= 006dce +bright5= c44392 +bright6= 00978a +bright7= 3a4d53 diff --git a/themes/selenized-black b/themes/selenized-black new file mode 100644 index 0000000..8a93187 --- /dev/null +++ b/themes/selenized-black @@ -0,0 +1,25 @@ +# -*- conf -*- +# Selenized black + +[colors-dark] +cursor = 181818 56d8c9 +background= 181818 +foreground= b9b9b9 + +regular0= 252525 +regular1= ed4a46 +regular2= 70b433 +regular3= dbb32d +regular4= 368aeb +regular5= eb6eb7 +regular6= 3fc5b7 +regular7= 777777 + +bright0= 3b3b3b +bright1= ff5e56 +bright2= 83c746 +bright3= efc541 +bright4= 4f9cfe +bright5= ff81ca +bright6= 56d8c9 +bright7= dedede diff --git a/themes/selenized-dark b/themes/selenized-dark new file mode 100644 index 0000000..8ace1c0 --- /dev/null +++ b/themes/selenized-dark @@ -0,0 +1,25 @@ +# -*- conf -*- +# Selenized dark + +[colors-dark] +cursor = 103c48 53d6c7 +background= 103c48 +foreground= adbcbc + +regular0= 184956 +regular1= fa5750 +regular2= 75b938 +regular3= dbb32d +regular4= 4695f7 +regular5= f275be +regular6= 41c7b9 +regular7= 72898f + +bright0= 2d5b69 +bright1= ff665c +bright2= 84c747 +bright3= ebc13d +bright4= 58a3ff +bright5= ff84cd +bright6= 53d6c7 +bright7= cad8d9 diff --git a/themes/selenized-light b/themes/selenized-light new file mode 100644 index 0000000..c842fc3 --- /dev/null +++ b/themes/selenized-light @@ -0,0 +1,28 @@ +# -*- conf -*- +# Selenized light + +[main] +initial-color-theme=light + +[colors-light] +cursor=fbf3db 00978a +background= fbf3db +foreground= 53676d + +regular0= ece3cc +regular1= d2212d +regular2= 489100 +regular3= ad8900 +regular4= 0072d4 +regular5= ca4898 +regular6= 009c8f +regular7= 909995 + +bright0= d5cdb6 +bright1= cc1729 +bright2= 428b00 +bright3= a78300 +bright4= 006dce +bright5= c44392 +bright6= 00978a +bright7= 3a4d53 diff --git a/themes/selenized-white b/themes/selenized-white new file mode 100644 index 0000000..659bf81 --- /dev/null +++ b/themes/selenized-white @@ -0,0 +1,28 @@ +# -*- conf -*- +# Selenized white + +[main] +initial-color-theme=light + +[colors-light] +cursor=ffffff 009a8a +background= ffffff +foreground= 474747 + +regular0= ebebeb +regular1= d6000c +regular2= 1d9700 +regular3= c49700 +regular4= 0064e4 +regular5= dd0f9d +regular6= 00ad9c +regular7= 878787 + +bright0= cdcdcd +bright1= bf0000 +bright2= 008400 +bright3= af8500 +bright4= 0054cf +bright5= c7008b +bright6= 009a8a +bright7= 282828 diff --git a/themes/solarized b/themes/solarized new file mode 100644 index 0000000..f1844b3 --- /dev/null +++ b/themes/solarized @@ -0,0 +1,47 @@ +# -*- conf -*- +# Solarized dark+light + +# Dark +[colors-dark] +cursor= 002b36 93a1a1 +background= 002b36 +foreground= 839496 +regular0= 073642 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= eee8d5 +bright0= 002b36 +bright1= cb4b16 +bright2= 586e75 +bright3= 657b83 +bright4= 839496 +bright5= 6c71c4 +bright6= 93a1a1 +bright7= fdf6e3 + + +# Light +[colors-light] +cursor= fdf6e3 586e75 +background= fdf6e3 +foreground= 657b83 +regular0= eee8d5 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= 073642 +bright0= cb4b16 +bright1= fdf6e3 +bright2= 93a1a1 +bright3= 839496 +bright4= 657b83 +bright5= 6c71c4 +bright6= 586e75 +bright7= 002b36 diff --git a/themes/solarized-dark b/themes/solarized-dark new file mode 100644 index 0000000..6335fa0 --- /dev/null +++ b/themes/solarized-dark @@ -0,0 +1,28 @@ +# -*- conf -*- +# Solarized dark + +[colors-dark] +cursor= 002b36 93a1a1 +background= 002b36 +foreground= 839496 +regular0= 073642 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= eee8d5 +bright0= 002b36 +bright1= cb4b16 +bright2= 586e75 +bright3= 657b83 +bright4= 839496 +bright5= 6c71c4 +bright6= 93a1a1 +bright7= fdf6e3 + +# Enable if prefer solarized colors instead of inverterd fg/bg for +# highlighting (mouse selection) +# selection-foreground=93a1a1 +# selection-background=073642 diff --git a/themes/solarized-dark-normal-brights b/themes/solarized-dark-normal-brights new file mode 100644 index 0000000..7b60811 --- /dev/null +++ b/themes/solarized-dark-normal-brights @@ -0,0 +1,30 @@ +# -*- conf -*- +# Solarized dark + +[colors-dark] +cursor= 002b36 93a1a1 +background= 002b36 +foreground= 839496 +regular0= 073642 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= eee8d5 + +# regularN brightened by increasing luminance by 20% +bright0= 08404f +bright1= e35f5c +bright2= 9fb700 +bright3= d9a400 +bright4= 4ba1de +bright5= dc619d +bright6= 32c1b6 +bright7= ffffff + +# Enable if prefer solarized colors instead of inverterd fg/bg for +# highlighting (mouse selection) +# selection-foreground=93a1a1 +# selection-background=073642 diff --git a/themes/solarized-light b/themes/solarized-light new file mode 100644 index 0000000..db27be4 --- /dev/null +++ b/themes/solarized-light @@ -0,0 +1,26 @@ +# -*- conf -*- +# Solarized light + +[main] +initial-color-theme=light + +[colors-light] +cursor= fdf6e3 586e75 +background= fdf6e3 +foreground= 657b83 +regular0= eee8d5 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= 073642 +bright0= cb4b16 +bright1= fdf6e3 +bright2= 93a1a1 +bright3= 839496 +bright4= 657b83 +bright5= 6c71c4 +bright6= 586e75 +bright7= 002b36 diff --git a/themes/solarized-normal-brights b/themes/solarized-normal-brights new file mode 100644 index 0000000..3bd3c18 --- /dev/null +++ b/themes/solarized-normal-brights @@ -0,0 +1,54 @@ +# -*- conf -*- +# Solarized dark+light +# +# Bright colors are brighter versions of the regular colors, instead +# of the normal solarized palette. +# +# They were generated by taking the regular colors, decoding from sRGB +# to linear, multiplying the linear RGB values by 1.8, and then +# encoding to sRGB again. + +# Dark +[colors-dark] +cursor= 002b36 93a1a1 +background= 002b36 +foreground= 839496 +regular0= 073642 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= eee8d5 +bright0= 0c4958 +bright1= ff4440 +bright2= aec700 +bright3= ebb300 +bright4= 34b5ff +bright5= ff49aa +bright6= 3ad2c6 +bright7= ffffff + + +# Light +[colors-light] +cursor= fdf6e3 586e75 +background= fdf6e3 +foreground= 657b83 +regular0= eee8d5 +regular1= dc322f +regular2= 859900 +regular3= b58900 +regular4= 268bd2 +regular5= d33682 +regular6= 2aa198 +regular7= 073642 +bright0= ffffff +bright1= ff4440 +bright2= aec700 +bright3= ebb300 +bright4= 34b5ff +bright5= ff49aa +bright6= 3ad2c6 +bright7= 0c4958 diff --git a/themes/srcery b/themes/srcery new file mode 100644 index 0000000..612c82c --- /dev/null +++ b/themes/srcery @@ -0,0 +1,26 @@ +# srcery + +[colors-dark] +background= 1c1b19 +foreground= fce8c3 +regular0= 1c1b19 +regular1= ef2f27 +regular2= 519f50 +regular3= fbb829 +regular4= 2c78bf +regular5= e02c6d +regular6= 0aaeb3 +regular7= baa67f +bright0= 918175 +bright1= f75341 +bright2= 98bc37 +bright3= fed06e +bright4= 68a8e4 +bright5= ff5c8f +bright6= 2be4d0 +bright7= fce8c3 + +## Enable if prefer solarized colors instead of inverterd fg/bg for +## highlighting (mouse selection) +# selection-foreground=93a1a1 +# selection-background=073642 diff --git a/themes/starlight b/themes/starlight new file mode 100644 index 0000000..81ce1a5 --- /dev/null +++ b/themes/starlight @@ -0,0 +1,24 @@ +# -*- conf -*- +# Theme: starlight V4 (https://github.com/CosmicToast/starlight) + +[colors-dark] +foreground = FFFFFF +background = 242424 + +regular0 = 242424 +regular1 = f62b5a +regular2 = 47b413 +regular3 = e3c401 +regular4 = 24acd4 +regular5 = f2affd +regular6 = 13c299 +regular7 = e6e6e6 + +bright0 = 616161 +bright1 = ff4d51 +bright2 = 35d450 +bright3 = e9e836 +bright4 = 5dc5f8 +bright5 = feabf2 +bright6 = 24dfc4 +bright7 = ffffff diff --git a/themes/tango b/themes/tango new file mode 100644 index 0000000..5ea43f6 --- /dev/null +++ b/themes/tango @@ -0,0 +1,23 @@ +# -*- conf -*- +# Tango + +[colors-dark] +cursor=000000 babdb6 +foreground=babdb6 +background=000000 +regular0=2e3436 +regular1=cc0000 +regular2=4e9a06 +regular3=c4a000 +regular4=3465a4 +regular5=75507b +regular6=06989a +regular7=d3d7cf +bright0=555753 +bright1=ef2929 +bright2=8ae234 +bright3=fce94f +bright4=729fcf +bright5=ad7fa8 +bright6=34e2e2 +bright7=eeeeec diff --git a/themes/tempus-autumn b/themes/tempus-autumn new file mode 100644 index 0000000..214478b --- /dev/null +++ b/themes/tempus-autumn @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Autumn +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with a palette inspired by earthly colours (WCAG AA compliant) + +[colors-dark] +#cursor = 302420 a9a2a6 +foreground = a9a2a6 +background = 302420 +regular0 = 302420 +regular1 = f46f55 +regular2 = 85a400 +regular3 = b09640 +regular4 = 799aca +regular5 = df798e +regular6 = 52a885 +regular7 = a8948a +bright0 = 36302a +bright1 = e27e3d +bright2 = 43aa7a +bright3 = ba9400 +bright4 = 958fdf +bright5 = ce7dc4 +bright6 = 2fa6b7 +bright7 = a9a2a6 +# selection-foreground = a8948a +# selection-background = 36302a diff --git a/themes/tempus-classic b/themes/tempus-classic new file mode 100644 index 0000000..95b37b7 --- /dev/null +++ b/themes/tempus-classic @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Classic +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with warm hues (WCAG AA compliant) + +[colors-dark] +#cursor = 232323 aeadaf +foreground = aeadaf +background = 232323 +regular0 = 232323 +regular1 = d4823d +regular2 = 8c9e3d +regular3 = b1942b +regular4 = 6e9cb0 +regular5 = b58d88 +regular6 = 6da280 +regular7 = 949d9f +bright0 = 312e30 +bright1 = d0913d +bright2 = 96a42d +bright3 = a8a030 +bright4 = 8e9cc0 +bright5 = d58888 +bright6 = 7aa880 +bright7 = aeadaf +# selection-foreground = 949d9f +# selection-background = 312e30 diff --git a/themes/tempus-dawn b/themes/tempus-dawn new file mode 100644 index 0000000..c288544 --- /dev/null +++ b/themes/tempus-dawn @@ -0,0 +1,31 @@ +# -*- conf -*- +# theme: Tempus Dawn +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Light theme with a soft, slightly desaturated palette (WCAG AA compliant) + +[main] +initial-color-theme=light + + +[colors-light] +#cursor = eff0f2 4a4b4e +foreground = 4a4b4e +background = eff0f2 +regular0 = 4a4b4e +regular1 = a32a3a +regular2 = 206620 +regular3 = 745300 +regular4 = 4b529a +regular5 = 8d377e +regular6 = 086784 +regular7 = dee2e0 +bright0 = 676364 +bright1 = a64822 +bright2 = 187408 +bright3 = 8b590a +bright4 = 5c59b2 +bright5 = 8e45a8 +bright6 = 3f649c +bright7 = eff0f2 +# selection-foreground = 676364 +# selection-background = dee2e0 diff --git a/themes/tempus-day b/themes/tempus-day new file mode 100644 index 0000000..03454f0 --- /dev/null +++ b/themes/tempus-day @@ -0,0 +1,30 @@ +# -*- conf -*- +# theme: Tempus Day +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Light theme with warm colours (WCAG AA compliant) + +[main] +initial-color-theme=light + +[colors-light] +#cursor = f8f2e5 464340 +foreground = 464340 +background = f8f2e5 +regular0 = 464340 +regular1 = c81000 +regular2 = 107410 +regular3 = 806000 +regular4 = 385dc4 +regular5 = b63052 +regular6 = 007070 +regular7 = e7e3d7 +bright0 = 68607d +bright1 = b24000 +bright2 = 427040 +bright3 = 6f6600 +bright4 = 0f64c4 +bright5 = 8050a7 +bright6 = 336c87 +bright7 = f8f2e5 +# selection-foreground = 68607d +# selection-background = e7e3d7 diff --git a/themes/tempus-dusk b/themes/tempus-dusk new file mode 100644 index 0000000..cd27aaa --- /dev/null +++ b/themes/tempus-dusk @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Dusk +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with a deep blue-ish, slightly desaturated palette (WCAG AA compliant) + +[colors-dark] +#cursor = 1f252d a2a8ba +foreground = a2a8ba +background = 1f252d +regular0 = 1f252d +regular1 = cb8d56 +regular2 = 8ba089 +regular3 = a79c46 +regular4 = 8c9abe +regular5 = b190af +regular6 = 8e9aba +regular7 = a29899 +bright0 = 2c3150 +bright1 = d39d74 +bright2 = 80b48f +bright3 = bda75a +bright4 = 9ca5de +bright5 = c69ac6 +bright6 = 8caeb6 +bright7 = a2a8ba +# selection-foreground = a29899 +# selection-background = 2c3150 diff --git a/themes/tempus-fugit b/themes/tempus-fugit new file mode 100644 index 0000000..b9dce35 --- /dev/null +++ b/themes/tempus-fugit @@ -0,0 +1,30 @@ +# -*- conf -*- +# theme: Tempus Fugit +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Light, pleasant theme optimised for long writing/coding sessions (WCAG AA compliant) + +[main] +initial-color-theme=light + +[colors-light] +#cursor = fff5f3 4d595f +foreground = 4d595f +background = fff5f3 +regular0 = 4d595f +regular1 = c61a14 +regular2 = 357200 +regular3 = 825e00 +regular4 = 1666b0 +regular5 = a83884 +regular6 = 007072 +regular7 = efe6e4 +bright0 = 796271 +bright1 = b93f1a +bright2 = 437520 +bright3 = 985900 +bright4 = 485adf +bright5 = a234c0 +bright6 = 00756a +bright7 = fff5f3 +# selection-foreground = 796271 +# selection-background = efe6e4 diff --git a/themes/tempus-future b/themes/tempus-future new file mode 100644 index 0000000..1f8c3c7 --- /dev/null +++ b/themes/tempus-future @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Future +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with colours inspired by concept art of outer space (WCAG AAA compliant) + +[colors-dark] +#cursor = 090a18 b4abac +foreground = b4abac +background = 090a18 +regular0 = 090a18 +regular1 = ff7e8f +regular2 = 6aba39 +regular3 = bfa51a +regular4 = 4ab2d7 +regular5 = e58f84 +regular6 = 2ab7bb +regular7 = a7a2c4 +bright0 = 2b1329 +bright1 = f78e2f +bright2 = 60ba80 +bright3 = de9b1d +bright4 = 8ba7ea +bright5 = e08bd6 +bright6 = 2cbab6 +bright7 = b4abac +# selection-foreground = a7a2c4 +# selection-background = 2b1329 diff --git a/themes/tempus-night b/themes/tempus-night new file mode 100644 index 0000000..aae80f0 --- /dev/null +++ b/themes/tempus-night @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Night +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: High contrast dark theme with bright colours (WCAG AAA compliant) + +[colors-dark] +#cursor = 1a1a1a e0e0e0 +foreground = e0e0e0 +background = 1a1a1a +regular0 = 1a1a1a +regular1 = ff929f +regular2 = 5fc940 +regular3 = c5b300 +regular4 = 5fb8ff +regular5 = ef91df +regular6 = 1dc5c3 +regular7 = c4bdaf +bright0 = 242536 +bright1 = f69d6a +bright2 = 88c400 +bright3 = d7ae00 +bright4 = 8cb4f0 +bright5 = de99f0 +bright6 = 00ca9a +bright7 = e0e0e0 +# selection-foreground = c4bdaf +# selection-background = 242536 diff --git a/themes/tempus-past b/themes/tempus-past new file mode 100644 index 0000000..5f90ddf --- /dev/null +++ b/themes/tempus-past @@ -0,0 +1,30 @@ +# -*- conf -*- +# theme: Tempus Past +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Light theme inspired by old vaporwave concept art (WCAG AA compliant) + +[main] +initial-color-theme=light + +[colors-light] +#cursor = f3f2f4 53545b +foreground = 53545b +background = f3f2f4 +regular0 = 53545b +regular1 = c00c50 +regular2 = 0a7040 +regular3 = a6403a +regular4 = 1763aa +regular5 = b02874 +regular6 = 096a83 +regular7 = eae2de +bright0 = 80565d +bright1 = bd3133 +bright2 = 337243 +bright3 = 8d554a +bright4 = 5559bb +bright5 = b022a7 +bright6 = 07707a +bright7 = f3f2f4 +# selection-foreground = 80565d +# selection-background = eae2de diff --git a/themes/tempus-rift b/themes/tempus-rift new file mode 100644 index 0000000..8add657 --- /dev/null +++ b/themes/tempus-rift @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Rift +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with a subdued palette on the green side of the spectrum (WCAG AA compliant) + +[colors-dark] +#cursor = 162c22 bbbcbc +foreground = bbbcbc +background = 162c22 +regular0 = 162c22 +regular1 = c19904 +regular2 = 34b534 +regular3 = 7fad00 +regular4 = 30aeb0 +regular5 = c8954c +regular6 = 5fad8f +regular7 = ab9aa9 +bright0 = 283431 +bright1 = d2a634 +bright2 = 6ac134 +bright3 = 82bd00 +bright4 = 56bdad +bright5 = cca0ba +bright6 = 10c480 +bright7 = bbbcbc +# selection-foreground = ab9aa9 +# selection-background = 283431 diff --git a/themes/tempus-spring b/themes/tempus-spring new file mode 100644 index 0000000..eb15a1b --- /dev/null +++ b/themes/tempus-spring @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Spring +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with a palette inspired by early spring colours (WCAG AA compliant) + +[colors-dark] +#cursor = 283a37 b5b8b7 +foreground = b5b8b7 +background = 283a37 +regular0 = 283a37 +regular1 = ff8b5f +regular2 = 5ec04d +regular3 = b0b01a +regular4 = 39bace +regular5 = e99399 +regular6 = 36c08e +regular7 = 99afae +bright0 = 2a453d +bright1 = e19e00 +bright2 = 73be0d +bright3 = c6a843 +bright4 = 70afef +bright5 = d095e2 +bright6 = 3cbfaf +bright7 = b5b8b7 +# selection-foreground = 99afae +# selection-background = 2a453d diff --git a/themes/tempus-summer b/themes/tempus-summer new file mode 100644 index 0000000..74c8faa --- /dev/null +++ b/themes/tempus-summer @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Summer +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with colours inspired by summer evenings by the sea (WCAG AA compliant) + +[colors-dark] +#cursor = 202c3d a0abae +foreground = a0abae +background = 202c3d +regular0 = 202c3d +regular1 = fe6f70 +regular2 = 4eb075 +regular3 = ba9a0a +regular4 = 60a1e6 +regular5 = d285ad +regular6 = 3dae9f +regular7 = 949cbf +bright0 = 39304f +bright1 = ec7f4f +bright2 = 5baf4f +bright3 = be981f +bright4 = 8599ef +bright5 = cc82d7 +bright6 = 2aacbf +bright7 = a0abae +# selection-foreground = 949cbf +# selection-background = 39304f diff --git a/themes/tempus-tempest b/themes/tempus-tempest new file mode 100644 index 0000000..f1cf55b --- /dev/null +++ b/themes/tempus-tempest @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Tempest +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: A green-scale, subtle theme for late night hackers (WCAG AAA compliant) + +[colors-dark] +#cursor = 282b2b b6e0ca +foreground = b6e0ca +background = 282b2b +regular0 = 282b2b +regular1 = cfc80a +regular2 = 7ad97a +regular3 = bfcc4a +regular4 = 60d7cd +regular5 = c5c4af +regular6 = 8bd0bf +regular7 = b0c8ca +bright0 = 323535 +bright1 = d1d933 +bright2 = 99e299 +bright3 = bbde4f +bright4 = 74e4cd +bright5 = d2d4aa +bright6 = 9bdfc4 +bright7 = b6e0ca +# selection-foreground = b0c8ca +# selection-background = 323535 diff --git a/themes/tempus-totus b/themes/tempus-totus new file mode 100644 index 0000000..fae6ede --- /dev/null +++ b/themes/tempus-totus @@ -0,0 +1,30 @@ +# -*- conf -*- +# theme: Tempus Totus +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Light theme for prose or for coding in an open space (WCAG AAA compliant) + +[main] +initial-color-theme=light + +[colors-light] +#cursor = ffffff 4a484d +foreground = 4a484d +background = ffffff +regular0 = 4a484d +regular1 = a50000 +regular2 = 005d26 +regular3 = 714700 +regular4 = 1d3ccf +regular5 = 88267a +regular6 = 185570 +regular7 = efefef +bright0 = 5e4b4f +bright1 = 992030 +bright2 = 4a5500 +bright3 = 8a3600 +bright4 = 2d45b0 +bright5 = 700dc9 +bright6 = 005289 +bright7 = ffffff +# selection-foreground = 5e4b4f +# selection-background = efefef diff --git a/themes/tempus-warp b/themes/tempus-warp new file mode 100644 index 0000000..906b3f3 --- /dev/null +++ b/themes/tempus-warp @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Warp +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with a vibrant palette (WCAG AA compliant) + +[colors-dark] +#cursor = 001514 a29fa0 +foreground = a29fa0 +background = 001514 +regular0 = 001514 +regular1 = ff3737 +regular2 = 169c16 +regular3 = 9f8500 +regular4 = 5781ef +regular5 = da4ebf +regular6 = 009880 +regular7 = 968282 +bright0 = 261c2c +bright1 = F0681A +bright2 = 3aa73a +bright3 = ba8a00 +bright4 = 8887f0 +bright5 = d85cf2 +bright6 = 1da1af +bright7 = a29fa0 +# selection-foreground = 968282 +# selection-background = 261c2c diff --git a/themes/tempus-winter b/themes/tempus-winter new file mode 100644 index 0000000..dc95128 --- /dev/null +++ b/themes/tempus-winter @@ -0,0 +1,27 @@ +# -*- conf -*- +# theme: Tempus Winter +# author: Protesilaos Stavrou (https://protesilaos.com) +# description: Dark theme with a palette inspired by winter nights at the city (WCAG AA compliant) + +[colors-dark] +#cursor = 202427 8da3b8 +foreground = 8da3b8 +background = 202427 +regular0 = 202427 +regular1 = ed6e5a +regular2 = 4aa920 +regular3 = 9a9921 +regular4 = 7b91df +regular5 = d17e80 +regular6 = 4fa394 +regular7 = 91959b +bright0 = 2a2e38 +bright1 = de7b28 +bright2 = 00ab5f +bright3 = af9155 +bright4 = 329fcb +bright5 = ca77c5 +bright6 = 1ba6a4 +bright7 = 8da3b8 +# selection-foreground = 91959b +# selection-background = 2a2e38 diff --git a/themes/tokyonight-light b/themes/tokyonight-light new file mode 100644 index 0000000..359a31b --- /dev/null +++ b/themes/tokyonight-light @@ -0,0 +1,28 @@ +# -*- conf -*- + +# Reference: https://github.com/tokyo-night/tokyo-night-vscode-theme/blob/master/themes/tokyo-night-light-color-theme.json + +[main] +initial-color-theme=light + +[colors-light] +background=d6d8df +foreground=343b58 +regular0=343b58 +regular1=8c4351 +regular2=33635c +regular3=8f5e15 +regular4=2959aa +regular5=7b43ba +regular6=006c86 +regular7=707280 +bright0=343b58 +bright1=8c4351 +bright2=33635c +bright3=8f5e15 +bright4=2959aa +bright5=7b43ba +bright6=006c86 +bright7=707280 + +jump-labels=343b58 e19d37 # brighter yellow than regular3 diff --git a/themes/tokyonight-night b/themes/tokyonight-night new file mode 100644 index 0000000..58037f7 --- /dev/null +++ b/themes/tokyonight-night @@ -0,0 +1,21 @@ +# -*- conf -*- + +[colors-dark] +background=1a1b26 +foreground=c0caf5 +regular0=15161E +regular1=f7768e +regular2=9ece6a +regular3=e0af68 +regular4=7aa2f7 +regular5=bb9af7 +regular6=7dcfff +regular7=a9b1d6 +bright0=414868 +bright1=f7768e +bright2=9ece6a +bright3=e0af68 +bright4=7aa2f7 +bright5=bb9af7 +bright6=7dcfff +bright7=c0caf5 diff --git a/themes/tokyonight-storm b/themes/tokyonight-storm new file mode 100644 index 0000000..4dbbf6c --- /dev/null +++ b/themes/tokyonight-storm @@ -0,0 +1,21 @@ +# -*- conf -*- + +[colors-dark] +background=24283b +foreground=c0caf5 +regular0=1D202F +regular1=f7768e +regular2=9ece6a +regular3=e0af68 +regular4=7aa2f7 +regular5=bb9af7 +regular6=7dcfff +regular7=a9b1d6 +bright0=414868 +bright1=f7768e +bright2=9ece6a +bright3=e0af68 +bright4=7aa2f7 +bright5=bb9af7 +bright6=7dcfff +bright7=c0caf5 diff --git a/themes/visibone b/themes/visibone new file mode 100644 index 0000000..b989b36 --- /dev/null +++ b/themes/visibone @@ -0,0 +1,23 @@ +# -*- conf -*- +# VisiBone + +[colors-dark] +cursor=010101 ffffff +foreground=ffffff +background=010101 +regular0=666666 +regular1=cc6666 +regular2=66cc99 +regular3=cc9966 +regular4=6699cc +regular5=cc6699 +regular6=66cccc +regular7=cccccc +bright0=999999 +bright1=ff9999 +bright2=99ffcc +bright3=ffcc99 +bright4=99ccff +bright5=ff99cc +bright6=99ffff +bright7=ffffff diff --git a/themes/xterm b/themes/xterm new file mode 100644 index 0000000..a9382fd --- /dev/null +++ b/themes/xterm @@ -0,0 +1,22 @@ +# -*- conf -*- +# The default palette of xterm. + +[colors-dark] +foreground=e5e5e5 +background=000000 +regular0=000000 # black +regular1=cd0000 # red +regular2=00cd00 # green +regular3=cdcd00 # yellow +regular4=0000ee # blue +regular5=cd00cd # magenta +regular6=00cdcd # cyan +regular7=e5e5e5 # white +bright0=7f7f7f # bright black +bright1=ff0000 # bright red +bright2=00ff00 # bright green +bright3=ffff00 # bright yellow +bright4=5c5cff # bright blue +bright5=ff00ff # bright magenta +bright6=00ffff # bright cyan +bright7=ffffff # bright white diff --git a/themes/zenburn b/themes/zenburn new file mode 100644 index 0000000..37a2681 --- /dev/null +++ b/themes/zenburn @@ -0,0 +1,25 @@ +# -*- conf -*- + +[colors-dark] +foreground=dcdccc +background=111111 + +## Normal/regular colors (color palette 0-7) +regular0=222222 # black +regular1=cc9393 # red +regular2=7f9f7f # green +regular3=d0bf8f # yellow +regular4=6ca0a3 # blue +regular5=dc8cc3 # magenta +regular6=93e0e3 # cyan +regular7=dcdccc # white + +## Bright colors (color palette 8-15) +bright0=666666 # bright black +bright1=dca3a3 # bright red +bright2=bfebbf # bright green +bright3=f0dfaf # bright yellow +bright4=8cd0d3 # bright blue +bright5=fcace3 # bright magenta +bright6=b3ffff # bright cyan +bright7=ffffff # bright white diff --git a/tokenize.c b/tokenize.c new file mode 100644 index 0000000..70ceb39 --- /dev/null +++ b/tokenize.c @@ -0,0 +1,103 @@ +#include "tokenize.h" + +#include +#include + +#define LOG_MODULE "tokenize" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "xmalloc.h" + +static bool +push_argv(char ***argv, size_t *size, const char *arg, size_t len, size_t *argc) +{ + if (arg != NULL && arg[0] == '%') + return true; + + if (*argc >= *size) { + size_t new_size = *size > 0 ? 2 * *size : 10; + char **new_argv = realloc(*argv, new_size * sizeof(new_argv[0])); + + if (new_argv == NULL) + return false; + + *argv = new_argv; + *size = new_size; + } + + (*argv)[(*argc)++] = arg != NULL ? xstrndup(arg, len) : NULL; + return true; +} + +bool +tokenize_cmdline(const char *cmdline, char ***argv) +{ + *argv = NULL; + size_t argv_size = 0; + + const char *final_end = cmdline + strlen(cmdline) + 1; + + bool first_token_is_quoted = cmdline[0] == '"' || cmdline[0] == '\''; + char delim = first_token_is_quoted ? cmdline[0] : ' '; + + const char *p = first_token_is_quoted ? &cmdline[1] : &cmdline[0]; + const char *search_start = p; + + size_t idx = 0; + while (*p != '\0') { + char *end = (char *)strchr(search_start, delim); + if (end == NULL) { + if (delim != ' ') { + LOG_ERR("unterminated %s quote", delim == '"' ? "double" : "single"); + goto err; + } + + if (!push_argv(argv, &argv_size, p, final_end - p, &idx) || + !push_argv(argv, &argv_size, NULL, 0, &idx)) + { + goto err; + } else + return true; + } + + if (end > p && *(end - 1) == '\\') { + /* Escaped quote, remove one level of escaping and + * continue searching for "our" closing quote */ + memmove(end - 1, end, strlen(end)); + end[strlen(end) - 1] = '\0'; + search_start = end; + continue; + } + + //*end = '\0'; + + if (!push_argv(argv, &argv_size, p, end - p, &idx)) + goto err; + + p = end + 1; + while (*p == delim) + p++; + + while (*p == ' ') + p++; + + if (*p == '"' || *p == '\'') { + delim = *p; + p++; + } else + delim = ' '; + search_start = p; + } + + if (!push_argv(argv, &argv_size, NULL, 0, &idx)) + goto err; + + return true; + +err: + for (size_t i = 0; i < idx; i++) + free((*argv)[i]); + free(*argv); + *argv = NULL; + return false; +} diff --git a/tokenize.h b/tokenize.h new file mode 100644 index 0000000..9e5de6c --- /dev/null +++ b/tokenize.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +bool tokenize_cmdline(const char *cmdline, char ***argv); diff --git a/unicode-mode.c b/unicode-mode.c new file mode 100644 index 0000000..1acdc66 --- /dev/null +++ b/unicode-mode.c @@ -0,0 +1,107 @@ +#include "unicode-mode.h" + +#define LOG_MODULE "unicode-input" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "render.h" +#include "search.h" + +void +unicode_mode_activate(struct terminal *term) +{ + if (term->unicode_mode.active) + return; + + term->unicode_mode.active = true; + term->unicode_mode.character = u'\0'; + term->unicode_mode.count = 0; + unicode_mode_updated(term); +} + +void +unicode_mode_deactivate(struct terminal *term) +{ + if (!term->unicode_mode.active) + return; + + term->unicode_mode.active = false; + unicode_mode_updated(term); +} + +void +unicode_mode_updated(struct terminal *term) +{ + if (term == NULL) + return; + + if (term->is_searching) + render_refresh_search(term); + else + render_refresh(term); +} + +void +unicode_mode_input(struct seat *seat, struct terminal *term, + xkb_keysym_t sym) +{ + if (sym == XKB_KEY_Return || + sym == XKB_KEY_space || + sym == XKB_KEY_KP_Enter || + sym == XKB_KEY_KP_Space) + { + char utf8[MB_CUR_MAX]; + size_t chars = c32rtomb( + utf8, term->unicode_mode.character, &(mbstate_t){0}); + + LOG_DBG("Unicode input: 0x%06x -> %.*s", + term->unicode_mode.character, (int)chars, utf8); + + if (chars != (size_t)-1) { + if (term->is_searching) + search_add_chars(term, utf8, chars); + else + term_to_slave(term, utf8, chars); + } + + unicode_mode_deactivate(term); + } + + else if (sym == XKB_KEY_Escape || + sym == XKB_KEY_q || + (seat->kbd.ctrl && (sym == XKB_KEY_c || + sym == XKB_KEY_d || + sym == XKB_KEY_g))) + { + unicode_mode_deactivate(term); + } + + else if (sym == XKB_KEY_BackSpace) { + if (term->unicode_mode.count > 0) { + term->unicode_mode.character >>= 4; + term->unicode_mode.count--; + unicode_mode_updated(term); + } + } + + else if (term->unicode_mode.count < 6) { + int digit = -1; + + /* 0-9, a-f, A-F */ + if (sym >= XKB_KEY_0 && sym <= XKB_KEY_9) + digit = sym - XKB_KEY_0; + else if (sym >= XKB_KEY_KP_0 && sym <= XKB_KEY_KP_9) + digit = sym - XKB_KEY_KP_0; + else if (sym >= XKB_KEY_a && sym <= XKB_KEY_f) + digit = 0xa + (sym - XKB_KEY_a); + else if (sym >= XKB_KEY_A && sym <= XKB_KEY_F) + digit = 0xa + (sym - XKB_KEY_A); + + if (digit >= 0) { + xassert(digit >= 0 && digit <= 0xf); + term->unicode_mode.character <<= 4; + term->unicode_mode.character |= digit; + term->unicode_mode.count++; + unicode_mode_updated(term); + } + } +} diff --git a/unicode-mode.h b/unicode-mode.h new file mode 100644 index 0000000..2f8d2b3 --- /dev/null +++ b/unicode-mode.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "terminal.h" + +void unicode_mode_activate(struct terminal *term); +void unicode_mode_deactivate(struct terminal *term); +void unicode_mode_updated(struct terminal *term); +void unicode_mode_input(struct seat *seat, struct terminal *term, + xkb_keysym_t sym); diff --git a/unicode/emoji-variation-sequences.txt b/unicode/emoji-variation-sequences.txt new file mode 100644 index 0000000..4373835 --- /dev/null +++ b/unicode/emoji-variation-sequences.txt @@ -0,0 +1,757 @@ +# emoji-variation-sequences.txt +# Date: 2024-05-01, 21:25:24 GMT +# © 2024 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use and license, see https://www.unicode.org/terms_of_use.html +# +# Emoji Variation Sequences for UTS #51 +# Used with Emoji Version 16.0 and subsequent minor revisions (if any) +# +# For documentation and usage, see https://www.unicode.org/reports/tr51 +# +0023 FE0E ; text style; # (1.1) NUMBER SIGN +0023 FE0F ; emoji style; # (1.1) NUMBER SIGN +002A FE0E ; text style; # (1.1) ASTERISK +002A FE0F ; emoji style; # (1.1) ASTERISK +0030 FE0E ; text style; # (1.1) DIGIT ZERO +0030 FE0F ; emoji style; # (1.1) DIGIT ZERO +0031 FE0E ; text style; # (1.1) DIGIT ONE +0031 FE0F ; emoji style; # (1.1) DIGIT ONE +0032 FE0E ; text style; # (1.1) DIGIT TWO +0032 FE0F ; emoji style; # (1.1) DIGIT TWO +0033 FE0E ; text style; # (1.1) DIGIT THREE +0033 FE0F ; emoji style; # (1.1) DIGIT THREE +0034 FE0E ; text style; # (1.1) DIGIT FOUR +0034 FE0F ; emoji style; # (1.1) DIGIT FOUR +0035 FE0E ; text style; # (1.1) DIGIT FIVE +0035 FE0F ; emoji style; # (1.1) DIGIT FIVE +0036 FE0E ; text style; # (1.1) DIGIT SIX +0036 FE0F ; emoji style; # (1.1) DIGIT SIX +0037 FE0E ; text style; # (1.1) DIGIT SEVEN +0037 FE0F ; emoji style; # (1.1) DIGIT SEVEN +0038 FE0E ; text style; # (1.1) DIGIT EIGHT +0038 FE0F ; emoji style; # (1.1) DIGIT EIGHT +0039 FE0E ; text style; # (1.1) DIGIT NINE +0039 FE0F ; emoji style; # (1.1) DIGIT NINE +00A9 FE0E ; text style; # (1.1) COPYRIGHT SIGN +00A9 FE0F ; emoji style; # (1.1) COPYRIGHT SIGN +00AE FE0E ; text style; # (1.1) REGISTERED SIGN +00AE FE0F ; emoji style; # (1.1) REGISTERED SIGN +203C FE0E ; text style; # (1.1) DOUBLE EXCLAMATION MARK +203C FE0F ; emoji style; # (1.1) DOUBLE EXCLAMATION MARK +2049 FE0E ; text style; # (3.0) EXCLAMATION QUESTION MARK +2049 FE0F ; emoji style; # (3.0) EXCLAMATION QUESTION MARK +2122 FE0E ; text style; # (1.1) TRADE MARK SIGN +2122 FE0F ; emoji style; # (1.1) TRADE MARK SIGN +2139 FE0E ; text style; # (3.0) INFORMATION SOURCE +2139 FE0F ; emoji style; # (3.0) INFORMATION SOURCE +2194 FE0E ; text style; # (1.1) LEFT RIGHT ARROW +2194 FE0F ; emoji style; # (1.1) LEFT RIGHT ARROW +2195 FE0E ; text style; # (1.1) UP DOWN ARROW +2195 FE0F ; emoji style; # (1.1) UP DOWN ARROW +2196 FE0E ; text style; # (1.1) NORTH WEST ARROW +2196 FE0F ; emoji style; # (1.1) NORTH WEST ARROW +2197 FE0E ; text style; # (1.1) NORTH EAST ARROW +2197 FE0F ; emoji style; # (1.1) NORTH EAST ARROW +2198 FE0E ; text style; # (1.1) SOUTH EAST ARROW +2198 FE0F ; emoji style; # (1.1) SOUTH EAST ARROW +2199 FE0E ; text style; # (1.1) SOUTH WEST ARROW +2199 FE0F ; emoji style; # (1.1) SOUTH WEST ARROW +21A9 FE0E ; text style; # (1.1) LEFTWARDS ARROW WITH HOOK +21A9 FE0F ; emoji style; # (1.1) LEFTWARDS ARROW WITH HOOK +21AA FE0E ; text style; # (1.1) RIGHTWARDS ARROW WITH HOOK +21AA FE0F ; emoji style; # (1.1) RIGHTWARDS ARROW WITH HOOK +231A FE0E ; text style; # (1.1) WATCH +231A FE0F ; emoji style; # (1.1) WATCH +231B FE0E ; text style; # (1.1) HOURGLASS +231B FE0F ; emoji style; # (1.1) HOURGLASS +2328 FE0E ; text style; # (1.1) KEYBOARD +2328 FE0F ; emoji style; # (1.1) KEYBOARD +23CF FE0E ; text style; # (4.0) EJECT SYMBOL +23CF FE0F ; emoji style; # (4.0) EJECT SYMBOL +23E9 FE0E ; text style; # (6.0) BLACK RIGHT-POINTING DOUBLE TRIANGLE +23E9 FE0F ; emoji style; # (6.0) BLACK RIGHT-POINTING DOUBLE TRIANGLE +23EA FE0E ; text style; # (6.0) BLACK LEFT-POINTING DOUBLE TRIANGLE +23EA FE0F ; emoji style; # (6.0) BLACK LEFT-POINTING DOUBLE TRIANGLE +23EB FE0E ; text style; # (6.0) BLACK UP-POINTING DOUBLE TRIANGLE +23EB FE0F ; emoji style; # (6.0) BLACK UP-POINTING DOUBLE TRIANGLE +23EC FE0E ; text style; # (6.0) BLACK DOWN-POINTING DOUBLE TRIANGLE +23EC FE0F ; emoji style; # (6.0) BLACK DOWN-POINTING DOUBLE TRIANGLE +23ED FE0E ; text style; # (6.0) BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR +23ED FE0F ; emoji style; # (6.0) BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR +23EE FE0E ; text style; # (6.0) BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR +23EE FE0F ; emoji style; # (6.0) BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR +23EF FE0E ; text style; # (6.0) BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR +23EF FE0F ; emoji style; # (6.0) BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR +23F0 FE0E ; text style; # (6.0) ALARM CLOCK +23F0 FE0F ; emoji style; # (6.0) ALARM CLOCK +23F1 FE0E ; text style; # (6.0) STOPWATCH +23F1 FE0F ; emoji style; # (6.0) STOPWATCH +23F2 FE0E ; text style; # (6.0) TIMER CLOCK +23F2 FE0F ; emoji style; # (6.0) TIMER CLOCK +23F3 FE0E ; text style; # (6.0) HOURGLASS WITH FLOWING SAND +23F3 FE0F ; emoji style; # (6.0) HOURGLASS WITH FLOWING SAND +23F8 FE0E ; text style; # (7.0) DOUBLE VERTICAL BAR +23F8 FE0F ; emoji style; # (7.0) DOUBLE VERTICAL BAR +23F9 FE0E ; text style; # (7.0) BLACK SQUARE FOR STOP +23F9 FE0F ; emoji style; # (7.0) BLACK SQUARE FOR STOP +23FA FE0E ; text style; # (7.0) BLACK CIRCLE FOR RECORD +23FA FE0F ; emoji style; # (7.0) BLACK CIRCLE FOR RECORD +24C2 FE0E ; text style; # (1.1) CIRCLED LATIN CAPITAL LETTER M +24C2 FE0F ; emoji style; # (1.1) CIRCLED LATIN CAPITAL LETTER M +25AA FE0E ; text style; # (1.1) BLACK SMALL SQUARE +25AA FE0F ; emoji style; # (1.1) BLACK SMALL SQUARE +25AB FE0E ; text style; # (1.1) WHITE SMALL SQUARE +25AB FE0F ; emoji style; # (1.1) WHITE SMALL SQUARE +25B6 FE0E ; text style; # (1.1) BLACK RIGHT-POINTING TRIANGLE +25B6 FE0F ; emoji style; # (1.1) BLACK RIGHT-POINTING TRIANGLE +25C0 FE0E ; text style; # (1.1) BLACK LEFT-POINTING TRIANGLE +25C0 FE0F ; emoji style; # (1.1) BLACK LEFT-POINTING TRIANGLE +25FB FE0E ; text style; # (3.2) WHITE MEDIUM SQUARE +25FB FE0F ; emoji style; # (3.2) WHITE MEDIUM SQUARE +25FC FE0E ; text style; # (3.2) BLACK MEDIUM SQUARE +25FC FE0F ; emoji style; # (3.2) BLACK MEDIUM SQUARE +25FD FE0E ; text style; # (3.2) WHITE MEDIUM SMALL SQUARE +25FD FE0F ; emoji style; # (3.2) WHITE MEDIUM SMALL SQUARE +25FE FE0E ; text style; # (3.2) BLACK MEDIUM SMALL SQUARE +25FE FE0F ; emoji style; # (3.2) BLACK MEDIUM SMALL SQUARE +2600 FE0E ; text style; # (1.1) BLACK SUN WITH RAYS +2600 FE0F ; emoji style; # (1.1) BLACK SUN WITH RAYS +2601 FE0E ; text style; # (1.1) CLOUD +2601 FE0F ; emoji style; # (1.1) CLOUD +2602 FE0E ; text style; # (1.1) UMBRELLA +2602 FE0F ; emoji style; # (1.1) UMBRELLA +2603 FE0E ; text style; # (1.1) SNOWMAN +2603 FE0F ; emoji style; # (1.1) SNOWMAN +2604 FE0E ; text style; # (1.1) COMET +2604 FE0F ; emoji style; # (1.1) COMET +260E FE0E ; text style; # (1.1) BLACK TELEPHONE +260E FE0F ; emoji style; # (1.1) BLACK TELEPHONE +2611 FE0E ; text style; # (1.1) BALLOT BOX WITH CHECK +2611 FE0F ; emoji style; # (1.1) BALLOT BOX WITH CHECK +2614 FE0E ; text style; # (4.0) UMBRELLA WITH RAIN DROPS +2614 FE0F ; emoji style; # (4.0) UMBRELLA WITH RAIN DROPS +2615 FE0E ; text style; # (4.0) HOT BEVERAGE +2615 FE0F ; emoji style; # (4.0) HOT BEVERAGE +2618 FE0E ; text style; # (4.1) SHAMROCK +2618 FE0F ; emoji style; # (4.1) SHAMROCK +261D FE0E ; text style; # (1.1) WHITE UP POINTING INDEX +261D FE0F ; emoji style; # (1.1) WHITE UP POINTING INDEX +2620 FE0E ; text style; # (1.1) SKULL AND CROSSBONES +2620 FE0F ; emoji style; # (1.1) SKULL AND CROSSBONES +2622 FE0E ; text style; # (1.1) RADIOACTIVE SIGN +2622 FE0F ; emoji style; # (1.1) RADIOACTIVE SIGN +2623 FE0E ; text style; # (1.1) BIOHAZARD SIGN +2623 FE0F ; emoji style; # (1.1) BIOHAZARD SIGN +2626 FE0E ; text style; # (1.1) ORTHODOX CROSS +2626 FE0F ; emoji style; # (1.1) ORTHODOX CROSS +262A FE0E ; text style; # (1.1) STAR AND CRESCENT +262A FE0F ; emoji style; # (1.1) STAR AND CRESCENT +262E FE0E ; text style; # (1.1) PEACE SYMBOL +262E FE0F ; emoji style; # (1.1) PEACE SYMBOL +262F FE0E ; text style; # (1.1) YIN YANG +262F FE0F ; emoji style; # (1.1) YIN YANG +2638 FE0E ; text style; # (1.1) WHEEL OF DHARMA +2638 FE0F ; emoji style; # (1.1) WHEEL OF DHARMA +2639 FE0E ; text style; # (1.1) WHITE FROWNING FACE +2639 FE0F ; emoji style; # (1.1) WHITE FROWNING FACE +263A FE0E ; text style; # (1.1) WHITE SMILING FACE +263A FE0F ; emoji style; # (1.1) WHITE SMILING FACE +2640 FE0E ; text style; # (1.1) FEMALE SIGN +2640 FE0F ; emoji style; # (1.1) FEMALE SIGN +2642 FE0E ; text style; # (1.1) MALE SIGN +2642 FE0F ; emoji style; # (1.1) MALE SIGN +2648 FE0E ; text style; # (1.1) ARIES +2648 FE0F ; emoji style; # (1.1) ARIES +2649 FE0E ; text style; # (1.1) TAURUS +2649 FE0F ; emoji style; # (1.1) TAURUS +264A FE0E ; text style; # (1.1) GEMINI +264A FE0F ; emoji style; # (1.1) GEMINI +264B FE0E ; text style; # (1.1) CANCER +264B FE0F ; emoji style; # (1.1) CANCER +264C FE0E ; text style; # (1.1) LEO +264C FE0F ; emoji style; # (1.1) LEO +264D FE0E ; text style; # (1.1) VIRGO +264D FE0F ; emoji style; # (1.1) VIRGO +264E FE0E ; text style; # (1.1) LIBRA +264E FE0F ; emoji style; # (1.1) LIBRA +264F FE0E ; text style; # (1.1) SCORPIUS +264F FE0F ; emoji style; # (1.1) SCORPIUS +2650 FE0E ; text style; # (1.1) SAGITTARIUS +2650 FE0F ; emoji style; # (1.1) SAGITTARIUS +2651 FE0E ; text style; # (1.1) CAPRICORN +2651 FE0F ; emoji style; # (1.1) CAPRICORN +2652 FE0E ; text style; # (1.1) AQUARIUS +2652 FE0F ; emoji style; # (1.1) AQUARIUS +2653 FE0E ; text style; # (1.1) PISCES +2653 FE0F ; emoji style; # (1.1) PISCES +265F FE0E ; text style; # (1.1) BLACK CHESS PAWN +265F FE0F ; emoji style; # (1.1) BLACK CHESS PAWN +2660 FE0E ; text style; # (1.1) BLACK SPADE SUIT +2660 FE0F ; emoji style; # (1.1) BLACK SPADE SUIT +2663 FE0E ; text style; # (1.1) BLACK CLUB SUIT +2663 FE0F ; emoji style; # (1.1) BLACK CLUB SUIT +2665 FE0E ; text style; # (1.1) BLACK HEART SUIT +2665 FE0F ; emoji style; # (1.1) BLACK HEART SUIT +2666 FE0E ; text style; # (1.1) BLACK DIAMOND SUIT +2666 FE0F ; emoji style; # (1.1) BLACK DIAMOND SUIT +2668 FE0E ; text style; # (1.1) HOT SPRINGS +2668 FE0F ; emoji style; # (1.1) HOT SPRINGS +267B FE0E ; text style; # (3.2) BLACK UNIVERSAL RECYCLING SYMBOL +267B FE0F ; emoji style; # (3.2) BLACK UNIVERSAL RECYCLING SYMBOL +267E FE0E ; text style; # (4.1) PERMANENT PAPER SIGN +267E FE0F ; emoji style; # (4.1) PERMANENT PAPER SIGN +267F FE0E ; text style; # (4.1) WHEELCHAIR SYMBOL +267F FE0F ; emoji style; # (4.1) WHEELCHAIR SYMBOL +2692 FE0E ; text style; # (4.1) HAMMER AND PICK +2692 FE0F ; emoji style; # (4.1) HAMMER AND PICK +2693 FE0E ; text style; # (4.1) ANCHOR +2693 FE0F ; emoji style; # (4.1) ANCHOR +2694 FE0E ; text style; # (4.1) CROSSED SWORDS +2694 FE0F ; emoji style; # (4.1) CROSSED SWORDS +2695 FE0E ; text style; # (4.1) STAFF OF AESCULAPIUS +2695 FE0F ; emoji style; # (4.1) STAFF OF AESCULAPIUS +2696 FE0E ; text style; # (4.1) SCALES +2696 FE0F ; emoji style; # (4.1) SCALES +2697 FE0E ; text style; # (4.1) ALEMBIC +2697 FE0F ; emoji style; # (4.1) ALEMBIC +2699 FE0E ; text style; # (4.1) GEAR +2699 FE0F ; emoji style; # (4.1) GEAR +269B FE0E ; text style; # (4.1) ATOM SYMBOL +269B FE0F ; emoji style; # (4.1) ATOM SYMBOL +269C FE0E ; text style; # (4.1) FLEUR-DE-LIS +269C FE0F ; emoji style; # (4.1) FLEUR-DE-LIS +26A0 FE0E ; text style; # (4.0) WARNING SIGN +26A0 FE0F ; emoji style; # (4.0) WARNING SIGN +26A1 FE0E ; text style; # (4.0) HIGH VOLTAGE SIGN +26A1 FE0F ; emoji style; # (4.0) HIGH VOLTAGE SIGN +26A7 FE0E ; text style; # (4.1) MALE WITH STROKE AND MALE AND FEMALE SIGN +26A7 FE0F ; emoji style; # (4.1) MALE WITH STROKE AND MALE AND FEMALE SIGN +26AA FE0E ; text style; # (4.1) MEDIUM WHITE CIRCLE +26AA FE0F ; emoji style; # (4.1) MEDIUM WHITE CIRCLE +26AB FE0E ; text style; # (4.1) MEDIUM BLACK CIRCLE +26AB FE0F ; emoji style; # (4.1) MEDIUM BLACK CIRCLE +26B0 FE0E ; text style; # (4.1) COFFIN +26B0 FE0F ; emoji style; # (4.1) COFFIN +26B1 FE0E ; text style; # (4.1) FUNERAL URN +26B1 FE0F ; emoji style; # (4.1) FUNERAL URN +26BD FE0E ; text style; # (5.2) SOCCER BALL +26BD FE0F ; emoji style; # (5.2) SOCCER BALL +26BE FE0E ; text style; # (5.2) BASEBALL +26BE FE0F ; emoji style; # (5.2) BASEBALL +26C4 FE0E ; text style; # (5.2) SNOWMAN WITHOUT SNOW +26C4 FE0F ; emoji style; # (5.2) SNOWMAN WITHOUT SNOW +26C5 FE0E ; text style; # (5.2) SUN BEHIND CLOUD +26C5 FE0F ; emoji style; # (5.2) SUN BEHIND CLOUD +26C8 FE0E ; text style; # (5.2) THUNDER CLOUD AND RAIN +26C8 FE0F ; emoji style; # (5.2) THUNDER CLOUD AND RAIN +26CE FE0E ; text style; # (6.0) OPHIUCHUS +26CE FE0F ; emoji style; # (6.0) OPHIUCHUS +26CF FE0E ; text style; # (5.2) PICK +26CF FE0F ; emoji style; # (5.2) PICK +26D1 FE0E ; text style; # (5.2) HELMET WITH WHITE CROSS +26D1 FE0F ; emoji style; # (5.2) HELMET WITH WHITE CROSS +26D3 FE0E ; text style; # (5.2) CHAINS +26D3 FE0F ; emoji style; # (5.2) CHAINS +26D4 FE0E ; text style; # (5.2) NO ENTRY +26D4 FE0F ; emoji style; # (5.2) NO ENTRY +26E9 FE0E ; text style; # (5.2) SHINTO SHRINE +26E9 FE0F ; emoji style; # (5.2) SHINTO SHRINE +26EA FE0E ; text style; # (5.2) CHURCH +26EA FE0F ; emoji style; # (5.2) CHURCH +26F0 FE0E ; text style; # (5.2) MOUNTAIN +26F0 FE0F ; emoji style; # (5.2) MOUNTAIN +26F1 FE0E ; text style; # (5.2) UMBRELLA ON GROUND +26F1 FE0F ; emoji style; # (5.2) UMBRELLA ON GROUND +26F2 FE0E ; text style; # (5.2) FOUNTAIN +26F2 FE0F ; emoji style; # (5.2) FOUNTAIN +26F3 FE0E ; text style; # (5.2) FLAG IN HOLE +26F3 FE0F ; emoji style; # (5.2) FLAG IN HOLE +26F4 FE0E ; text style; # (5.2) FERRY +26F4 FE0F ; emoji style; # (5.2) FERRY +26F5 FE0E ; text style; # (5.2) SAILBOAT +26F5 FE0F ; emoji style; # (5.2) SAILBOAT +26F7 FE0E ; text style; # (5.2) SKIER +26F7 FE0F ; emoji style; # (5.2) SKIER +26F8 FE0E ; text style; # (5.2) ICE SKATE +26F8 FE0F ; emoji style; # (5.2) ICE SKATE +26F9 FE0E ; text style; # (5.2) PERSON WITH BALL +26F9 FE0F ; emoji style; # (5.2) PERSON WITH BALL +26FA FE0E ; text style; # (5.2) TENT +26FA FE0F ; emoji style; # (5.2) TENT +26FD FE0E ; text style; # (5.2) FUEL PUMP +26FD FE0F ; emoji style; # (5.2) FUEL PUMP +2702 FE0E ; text style; # (1.1) BLACK SCISSORS +2702 FE0F ; emoji style; # (1.1) BLACK SCISSORS +2705 FE0E ; text style; # (6.0) WHITE HEAVY CHECK MARK +2705 FE0F ; emoji style; # (6.0) WHITE HEAVY CHECK MARK +2708 FE0E ; text style; # (1.1) AIRPLANE +2708 FE0F ; emoji style; # (1.1) AIRPLANE +2709 FE0E ; text style; # (1.1) ENVELOPE +2709 FE0F ; emoji style; # (1.1) ENVELOPE +270A FE0E ; text style; # (6.0) RAISED FIST +270A FE0F ; emoji style; # (6.0) RAISED FIST +270B FE0E ; text style; # (6.0) RAISED HAND +270B FE0F ; emoji style; # (6.0) RAISED HAND +270C FE0E ; text style; # (1.1) VICTORY HAND +270C FE0F ; emoji style; # (1.1) VICTORY HAND +270D FE0E ; text style; # (1.1) WRITING HAND +270D FE0F ; emoji style; # (1.1) WRITING HAND +270F FE0E ; text style; # (1.1) PENCIL +270F FE0F ; emoji style; # (1.1) PENCIL +2712 FE0E ; text style; # (1.1) BLACK NIB +2712 FE0F ; emoji style; # (1.1) BLACK NIB +2714 FE0E ; text style; # (1.1) HEAVY CHECK MARK +2714 FE0F ; emoji style; # (1.1) HEAVY CHECK MARK +2716 FE0E ; text style; # (1.1) HEAVY MULTIPLICATION X +2716 FE0F ; emoji style; # (1.1) HEAVY MULTIPLICATION X +271D FE0E ; text style; # (1.1) LATIN CROSS +271D FE0F ; emoji style; # (1.1) LATIN CROSS +2721 FE0E ; text style; # (1.1) STAR OF DAVID +2721 FE0F ; emoji style; # (1.1) STAR OF DAVID +2728 FE0E ; text style; # (6.0) SPARKLES +2728 FE0F ; emoji style; # (6.0) SPARKLES +2733 FE0E ; text style; # (1.1) EIGHT SPOKED ASTERISK +2733 FE0F ; emoji style; # (1.1) EIGHT SPOKED ASTERISK +2734 FE0E ; text style; # (1.1) EIGHT POINTED BLACK STAR +2734 FE0F ; emoji style; # (1.1) EIGHT POINTED BLACK STAR +2744 FE0E ; text style; # (1.1) SNOWFLAKE +2744 FE0F ; emoji style; # (1.1) SNOWFLAKE +2747 FE0E ; text style; # (1.1) SPARKLE +2747 FE0F ; emoji style; # (1.1) SPARKLE +274C FE0E ; text style; # (6.0) CROSS MARK +274C FE0F ; emoji style; # (6.0) CROSS MARK +274E FE0E ; text style; # (6.0) NEGATIVE SQUARED CROSS MARK +274E FE0F ; emoji style; # (6.0) NEGATIVE SQUARED CROSS MARK +2753 FE0E ; text style; # (6.0) BLACK QUESTION MARK ORNAMENT +2753 FE0F ; emoji style; # (6.0) BLACK QUESTION MARK ORNAMENT +2754 FE0E ; text style; # (6.0) WHITE QUESTION MARK ORNAMENT +2754 FE0F ; emoji style; # (6.0) WHITE QUESTION MARK ORNAMENT +2755 FE0E ; text style; # (6.0) WHITE EXCLAMATION MARK ORNAMENT +2755 FE0F ; emoji style; # (6.0) WHITE EXCLAMATION MARK ORNAMENT +2757 FE0E ; text style; # (5.2) HEAVY EXCLAMATION MARK SYMBOL +2757 FE0F ; emoji style; # (5.2) HEAVY EXCLAMATION MARK SYMBOL +2763 FE0E ; text style; # (1.1) HEAVY HEART EXCLAMATION MARK ORNAMENT +2763 FE0F ; emoji style; # (1.1) HEAVY HEART EXCLAMATION MARK ORNAMENT +2764 FE0E ; text style; # (1.1) HEAVY BLACK HEART +2764 FE0F ; emoji style; # (1.1) HEAVY BLACK HEART +2795 FE0E ; text style; # (6.0) HEAVY PLUS SIGN +2795 FE0F ; emoji style; # (6.0) HEAVY PLUS SIGN +2796 FE0E ; text style; # (6.0) HEAVY MINUS SIGN +2796 FE0F ; emoji style; # (6.0) HEAVY MINUS SIGN +2797 FE0E ; text style; # (6.0) HEAVY DIVISION SIGN +2797 FE0F ; emoji style; # (6.0) HEAVY DIVISION SIGN +27A1 FE0E ; text style; # (1.1) BLACK RIGHTWARDS ARROW +27A1 FE0F ; emoji style; # (1.1) BLACK RIGHTWARDS ARROW +27B0 FE0E ; text style; # (6.0) CURLY LOOP +27B0 FE0F ; emoji style; # (6.0) CURLY LOOP +27BF FE0E ; text style; # (6.0) DOUBLE CURLY LOOP +27BF FE0F ; emoji style; # (6.0) DOUBLE CURLY LOOP +2934 FE0E ; text style; # (3.2) ARROW POINTING RIGHTWARDS THEN CURVING UPWARDS +2934 FE0F ; emoji style; # (3.2) ARROW POINTING RIGHTWARDS THEN CURVING UPWARDS +2935 FE0E ; text style; # (3.2) ARROW POINTING RIGHTWARDS THEN CURVING DOWNWARDS +2935 FE0F ; emoji style; # (3.2) ARROW POINTING RIGHTWARDS THEN CURVING DOWNWARDS +2B05 FE0E ; text style; # (4.0) LEFTWARDS BLACK ARROW +2B05 FE0F ; emoji style; # (4.0) LEFTWARDS BLACK ARROW +2B06 FE0E ; text style; # (4.0) UPWARDS BLACK ARROW +2B06 FE0F ; emoji style; # (4.0) UPWARDS BLACK ARROW +2B07 FE0E ; text style; # (4.0) DOWNWARDS BLACK ARROW +2B07 FE0F ; emoji style; # (4.0) DOWNWARDS BLACK ARROW +2B1B FE0E ; text style; # (5.1) BLACK LARGE SQUARE +2B1B FE0F ; emoji style; # (5.1) BLACK LARGE SQUARE +2B1C FE0E ; text style; # (5.1) WHITE LARGE SQUARE +2B1C FE0F ; emoji style; # (5.1) WHITE LARGE SQUARE +2B50 FE0E ; text style; # (5.1) WHITE MEDIUM STAR +2B50 FE0F ; emoji style; # (5.1) WHITE MEDIUM STAR +2B55 FE0E ; text style; # (5.2) HEAVY LARGE CIRCLE +2B55 FE0F ; emoji style; # (5.2) HEAVY LARGE CIRCLE +3030 FE0E ; text style; # (1.1) WAVY DASH +3030 FE0F ; emoji style; # (1.1) WAVY DASH +303D FE0E ; text style; # (3.2) PART ALTERNATION MARK +303D FE0F ; emoji style; # (3.2) PART ALTERNATION MARK +3297 FE0E ; text style; # (1.1) CIRCLED IDEOGRAPH CONGRATULATION +3297 FE0F ; emoji style; # (1.1) CIRCLED IDEOGRAPH CONGRATULATION +3299 FE0E ; text style; # (1.1) CIRCLED IDEOGRAPH SECRET +3299 FE0F ; emoji style; # (1.1) CIRCLED IDEOGRAPH SECRET +1F004 FE0E ; text style; # (5.1) MAHJONG TILE RED DRAGON +1F004 FE0F ; emoji style; # (5.1) MAHJONG TILE RED DRAGON +1F170 FE0E ; text style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER A +1F170 FE0F ; emoji style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER A +1F171 FE0E ; text style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER B +1F171 FE0F ; emoji style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER B +1F17E FE0E ; text style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER O +1F17E FE0F ; emoji style; # (6.0) NEGATIVE SQUARED LATIN CAPITAL LETTER O +1F17F FE0E ; text style; # (5.2) NEGATIVE SQUARED LATIN CAPITAL LETTER P +1F17F FE0F ; emoji style; # (5.2) NEGATIVE SQUARED LATIN CAPITAL LETTER P +1F202 FE0E ; text style; # (6.0) SQUARED KATAKANA SA +1F202 FE0F ; emoji style; # (6.0) SQUARED KATAKANA SA +1F21A FE0E ; text style; # (5.2) SQUARED CJK UNIFIED IDEOGRAPH-7121 +1F21A FE0F ; emoji style; # (5.2) SQUARED CJK UNIFIED IDEOGRAPH-7121 +1F22F FE0E ; text style; # (5.2) SQUARED CJK UNIFIED IDEOGRAPH-6307 +1F22F FE0F ; emoji style; # (5.2) SQUARED CJK UNIFIED IDEOGRAPH-6307 +1F237 FE0E ; text style; # (6.0) SQUARED CJK UNIFIED IDEOGRAPH-6708 +1F237 FE0F ; emoji style; # (6.0) SQUARED CJK UNIFIED IDEOGRAPH-6708 +1F30D FE0E ; text style; # (6.0) EARTH GLOBE EUROPE-AFRICA +1F30D FE0F ; emoji style; # (6.0) EARTH GLOBE EUROPE-AFRICA +1F30E FE0E ; text style; # (6.0) EARTH GLOBE AMERICAS +1F30E FE0F ; emoji style; # (6.0) EARTH GLOBE AMERICAS +1F30F FE0E ; text style; # (6.0) EARTH GLOBE ASIA-AUSTRALIA +1F30F FE0F ; emoji style; # (6.0) EARTH GLOBE ASIA-AUSTRALIA +1F315 FE0E ; text style; # (6.0) FULL MOON SYMBOL +1F315 FE0F ; emoji style; # (6.0) FULL MOON SYMBOL +1F31C FE0E ; text style; # (6.0) LAST QUARTER MOON WITH FACE +1F31C FE0F ; emoji style; # (6.0) LAST QUARTER MOON WITH FACE +1F321 FE0E ; text style; # (7.0) THERMOMETER +1F321 FE0F ; emoji style; # (7.0) THERMOMETER +1F324 FE0E ; text style; # (7.0) WHITE SUN WITH SMALL CLOUD +1F324 FE0F ; emoji style; # (7.0) WHITE SUN WITH SMALL CLOUD +1F325 FE0E ; text style; # (7.0) WHITE SUN BEHIND CLOUD +1F325 FE0F ; emoji style; # (7.0) WHITE SUN BEHIND CLOUD +1F326 FE0E ; text style; # (7.0) WHITE SUN BEHIND CLOUD WITH RAIN +1F326 FE0F ; emoji style; # (7.0) WHITE SUN BEHIND CLOUD WITH RAIN +1F327 FE0E ; text style; # (7.0) CLOUD WITH RAIN +1F327 FE0F ; emoji style; # (7.0) CLOUD WITH RAIN +1F328 FE0E ; text style; # (7.0) CLOUD WITH SNOW +1F328 FE0F ; emoji style; # (7.0) CLOUD WITH SNOW +1F329 FE0E ; text style; # (7.0) CLOUD WITH LIGHTNING +1F329 FE0F ; emoji style; # (7.0) CLOUD WITH LIGHTNING +1F32A FE0E ; text style; # (7.0) CLOUD WITH TORNADO +1F32A FE0F ; emoji style; # (7.0) CLOUD WITH TORNADO +1F32B FE0E ; text style; # (7.0) FOG +1F32B FE0F ; emoji style; # (7.0) FOG +1F32C FE0E ; text style; # (7.0) WIND BLOWING FACE +1F32C FE0F ; emoji style; # (7.0) WIND BLOWING FACE +1F336 FE0E ; text style; # (7.0) HOT PEPPER +1F336 FE0F ; emoji style; # (7.0) HOT PEPPER +1F378 FE0E ; text style; # (6.0) COCKTAIL GLASS +1F378 FE0F ; emoji style; # (6.0) COCKTAIL GLASS +1F37D FE0E ; text style; # (7.0) FORK AND KNIFE WITH PLATE +1F37D FE0F ; emoji style; # (7.0) FORK AND KNIFE WITH PLATE +1F393 FE0E ; text style; # (6.0) GRADUATION CAP +1F393 FE0F ; emoji style; # (6.0) GRADUATION CAP +1F396 FE0E ; text style; # (7.0) MILITARY MEDAL +1F396 FE0F ; emoji style; # (7.0) MILITARY MEDAL +1F397 FE0E ; text style; # (7.0) REMINDER RIBBON +1F397 FE0F ; emoji style; # (7.0) REMINDER RIBBON +1F399 FE0E ; text style; # (7.0) STUDIO MICROPHONE +1F399 FE0F ; emoji style; # (7.0) STUDIO MICROPHONE +1F39A FE0E ; text style; # (7.0) LEVEL SLIDER +1F39A FE0F ; emoji style; # (7.0) LEVEL SLIDER +1F39B FE0E ; text style; # (7.0) CONTROL KNOBS +1F39B FE0F ; emoji style; # (7.0) CONTROL KNOBS +1F39E FE0E ; text style; # (7.0) FILM FRAMES +1F39E FE0F ; emoji style; # (7.0) FILM FRAMES +1F39F FE0E ; text style; # (7.0) ADMISSION TICKETS +1F39F FE0F ; emoji style; # (7.0) ADMISSION TICKETS +1F3A7 FE0E ; text style; # (6.0) HEADPHONE +1F3A7 FE0F ; emoji style; # (6.0) HEADPHONE +1F3AC FE0E ; text style; # (6.0) CLAPPER BOARD +1F3AC FE0F ; emoji style; # (6.0) CLAPPER BOARD +1F3AD FE0E ; text style; # (6.0) PERFORMING ARTS +1F3AD FE0F ; emoji style; # (6.0) PERFORMING ARTS +1F3AE FE0E ; text style; # (6.0) VIDEO GAME +1F3AE FE0F ; emoji style; # (6.0) VIDEO GAME +1F3C2 FE0E ; text style; # (6.0) SNOWBOARDER +1F3C2 FE0F ; emoji style; # (6.0) SNOWBOARDER +1F3C4 FE0E ; text style; # (6.0) SURFER +1F3C4 FE0F ; emoji style; # (6.0) SURFER +1F3C6 FE0E ; text style; # (6.0) TROPHY +1F3C6 FE0F ; emoji style; # (6.0) TROPHY +1F3CA FE0E ; text style; # (6.0) SWIMMER +1F3CA FE0F ; emoji style; # (6.0) SWIMMER +1F3CB FE0E ; text style; # (7.0) WEIGHT LIFTER +1F3CB FE0F ; emoji style; # (7.0) WEIGHT LIFTER +1F3CC FE0E ; text style; # (7.0) GOLFER +1F3CC FE0F ; emoji style; # (7.0) GOLFER +1F3CD FE0E ; text style; # (7.0) RACING MOTORCYCLE +1F3CD FE0F ; emoji style; # (7.0) RACING MOTORCYCLE +1F3CE FE0E ; text style; # (7.0) RACING CAR +1F3CE FE0F ; emoji style; # (7.0) RACING CAR +1F3D4 FE0E ; text style; # (7.0) SNOW CAPPED MOUNTAIN +1F3D4 FE0F ; emoji style; # (7.0) SNOW CAPPED MOUNTAIN +1F3D5 FE0E ; text style; # (7.0) CAMPING +1F3D5 FE0F ; emoji style; # (7.0) CAMPING +1F3D6 FE0E ; text style; # (7.0) BEACH WITH UMBRELLA +1F3D6 FE0F ; emoji style; # (7.0) BEACH WITH UMBRELLA +1F3D7 FE0E ; text style; # (7.0) BUILDING CONSTRUCTION +1F3D7 FE0F ; emoji style; # (7.0) BUILDING CONSTRUCTION +1F3D8 FE0E ; text style; # (7.0) HOUSE BUILDINGS +1F3D8 FE0F ; emoji style; # (7.0) HOUSE BUILDINGS +1F3D9 FE0E ; text style; # (7.0) CITYSCAPE +1F3D9 FE0F ; emoji style; # (7.0) CITYSCAPE +1F3DA FE0E ; text style; # (7.0) DERELICT HOUSE BUILDING +1F3DA FE0F ; emoji style; # (7.0) DERELICT HOUSE BUILDING +1F3DB FE0E ; text style; # (7.0) CLASSICAL BUILDING +1F3DB FE0F ; emoji style; # (7.0) CLASSICAL BUILDING +1F3DC FE0E ; text style; # (7.0) DESERT +1F3DC FE0F ; emoji style; # (7.0) DESERT +1F3DD FE0E ; text style; # (7.0) DESERT ISLAND +1F3DD FE0F ; emoji style; # (7.0) DESERT ISLAND +1F3DE FE0E ; text style; # (7.0) NATIONAL PARK +1F3DE FE0F ; emoji style; # (7.0) NATIONAL PARK +1F3DF FE0E ; text style; # (7.0) STADIUM +1F3DF FE0F ; emoji style; # (7.0) STADIUM +1F3E0 FE0E ; text style; # (6.0) HOUSE BUILDING +1F3E0 FE0F ; emoji style; # (6.0) HOUSE BUILDING +1F3ED FE0E ; text style; # (6.0) FACTORY +1F3ED FE0F ; emoji style; # (6.0) FACTORY +1F3F3 FE0E ; text style; # (7.0) WAVING WHITE FLAG +1F3F3 FE0F ; emoji style; # (7.0) WAVING WHITE FLAG +1F3F5 FE0E ; text style; # (7.0) ROSETTE +1F3F5 FE0F ; emoji style; # (7.0) ROSETTE +1F3F7 FE0E ; text style; # (7.0) LABEL +1F3F7 FE0F ; emoji style; # (7.0) LABEL +1F408 FE0E ; text style; # (6.0) CAT +1F408 FE0F ; emoji style; # (6.0) CAT +1F415 FE0E ; text style; # (6.0) DOG +1F415 FE0F ; emoji style; # (6.0) DOG +1F41F FE0E ; text style; # (6.0) FISH +1F41F FE0F ; emoji style; # (6.0) FISH +1F426 FE0E ; text style; # (6.0) BIRD +1F426 FE0F ; emoji style; # (6.0) BIRD +1F43F FE0E ; text style; # (7.0) CHIPMUNK +1F43F FE0F ; emoji style; # (7.0) CHIPMUNK +1F441 FE0E ; text style; # (7.0) EYE +1F441 FE0F ; emoji style; # (7.0) EYE +1F442 FE0E ; text style; # (6.0) EAR +1F442 FE0F ; emoji style; # (6.0) EAR +1F446 FE0E ; text style; # (6.0) WHITE UP POINTING BACKHAND INDEX +1F446 FE0F ; emoji style; # (6.0) WHITE UP POINTING BACKHAND INDEX +1F447 FE0E ; text style; # (6.0) WHITE DOWN POINTING BACKHAND INDEX +1F447 FE0F ; emoji style; # (6.0) WHITE DOWN POINTING BACKHAND INDEX +1F448 FE0E ; text style; # (6.0) WHITE LEFT POINTING BACKHAND INDEX +1F448 FE0F ; emoji style; # (6.0) WHITE LEFT POINTING BACKHAND INDEX +1F449 FE0E ; text style; # (6.0) WHITE RIGHT POINTING BACKHAND INDEX +1F449 FE0F ; emoji style; # (6.0) WHITE RIGHT POINTING BACKHAND INDEX +1F44D FE0E ; text style; # (6.0) THUMBS UP SIGN +1F44D FE0F ; emoji style; # (6.0) THUMBS UP SIGN +1F44E FE0E ; text style; # (6.0) THUMBS DOWN SIGN +1F44E FE0F ; emoji style; # (6.0) THUMBS DOWN SIGN +1F453 FE0E ; text style; # (6.0) EYEGLASSES +1F453 FE0F ; emoji style; # (6.0) EYEGLASSES +1F46A FE0E ; text style; # (6.0) FAMILY +1F46A FE0F ; emoji style; # (6.0) FAMILY +1F47D FE0E ; text style; # (6.0) EXTRATERRESTRIAL ALIEN +1F47D FE0F ; emoji style; # (6.0) EXTRATERRESTRIAL ALIEN +1F4A3 FE0E ; text style; # (6.0) BOMB +1F4A3 FE0F ; emoji style; # (6.0) BOMB +1F4B0 FE0E ; text style; # (6.0) MONEY BAG +1F4B0 FE0F ; emoji style; # (6.0) MONEY BAG +1F4B3 FE0E ; text style; # (6.0) CREDIT CARD +1F4B3 FE0F ; emoji style; # (6.0) CREDIT CARD +1F4BB FE0E ; text style; # (6.0) PERSONAL COMPUTER +1F4BB FE0F ; emoji style; # (6.0) PERSONAL COMPUTER +1F4BF FE0E ; text style; # (6.0) OPTICAL DISC +1F4BF FE0F ; emoji style; # (6.0) OPTICAL DISC +1F4CB FE0E ; text style; # (6.0) CLIPBOARD +1F4CB FE0F ; emoji style; # (6.0) CLIPBOARD +1F4DA FE0E ; text style; # (6.0) BOOKS +1F4DA FE0F ; emoji style; # (6.0) BOOKS +1F4DF FE0E ; text style; # (6.0) PAGER +1F4DF FE0F ; emoji style; # (6.0) PAGER +1F4E4 FE0E ; text style; # (6.0) OUTBOX TRAY +1F4E4 FE0F ; emoji style; # (6.0) OUTBOX TRAY +1F4E5 FE0E ; text style; # (6.0) INBOX TRAY +1F4E5 FE0F ; emoji style; # (6.0) INBOX TRAY +1F4E6 FE0E ; text style; # (6.0) PACKAGE +1F4E6 FE0F ; emoji style; # (6.0) PACKAGE +1F4EA FE0E ; text style; # (6.0) CLOSED MAILBOX WITH LOWERED FLAG +1F4EA FE0F ; emoji style; # (6.0) CLOSED MAILBOX WITH LOWERED FLAG +1F4EB FE0E ; text style; # (6.0) CLOSED MAILBOX WITH RAISED FLAG +1F4EB FE0F ; emoji style; # (6.0) CLOSED MAILBOX WITH RAISED FLAG +1F4EC FE0E ; text style; # (6.0) OPEN MAILBOX WITH RAISED FLAG +1F4EC FE0F ; emoji style; # (6.0) OPEN MAILBOX WITH RAISED FLAG +1F4ED FE0E ; text style; # (6.0) OPEN MAILBOX WITH LOWERED FLAG +1F4ED FE0F ; emoji style; # (6.0) OPEN MAILBOX WITH LOWERED FLAG +1F4F7 FE0E ; text style; # (6.0) CAMERA +1F4F7 FE0F ; emoji style; # (6.0) CAMERA +1F4F9 FE0E ; text style; # (6.0) VIDEO CAMERA +1F4F9 FE0F ; emoji style; # (6.0) VIDEO CAMERA +1F4FA FE0E ; text style; # (6.0) TELEVISION +1F4FA FE0F ; emoji style; # (6.0) TELEVISION +1F4FB FE0E ; text style; # (6.0) RADIO +1F4FB FE0F ; emoji style; # (6.0) RADIO +1F4FD FE0E ; text style; # (7.0) FILM PROJECTOR +1F4FD FE0F ; emoji style; # (7.0) FILM PROJECTOR +1F508 FE0E ; text style; # (6.0) SPEAKER +1F508 FE0F ; emoji style; # (6.0) SPEAKER +1F50D FE0E ; text style; # (6.0) LEFT-POINTING MAGNIFYING GLASS +1F50D FE0F ; emoji style; # (6.0) LEFT-POINTING MAGNIFYING GLASS +1F512 FE0E ; text style; # (6.0) LOCK +1F512 FE0F ; emoji style; # (6.0) LOCK +1F513 FE0E ; text style; # (6.0) OPEN LOCK +1F513 FE0F ; emoji style; # (6.0) OPEN LOCK +1F549 FE0E ; text style; # (7.0) OM SYMBOL +1F549 FE0F ; emoji style; # (7.0) OM SYMBOL +1F54A FE0E ; text style; # (7.0) DOVE OF PEACE +1F54A FE0F ; emoji style; # (7.0) DOVE OF PEACE +1F550 FE0E ; text style; # (6.0) CLOCK FACE ONE OCLOCK +1F550 FE0F ; emoji style; # (6.0) CLOCK FACE ONE OCLOCK +1F551 FE0E ; text style; # (6.0) CLOCK FACE TWO OCLOCK +1F551 FE0F ; emoji style; # (6.0) CLOCK FACE TWO OCLOCK +1F552 FE0E ; text style; # (6.0) CLOCK FACE THREE OCLOCK +1F552 FE0F ; emoji style; # (6.0) CLOCK FACE THREE OCLOCK +1F553 FE0E ; text style; # (6.0) CLOCK FACE FOUR OCLOCK +1F553 FE0F ; emoji style; # (6.0) CLOCK FACE FOUR OCLOCK +1F554 FE0E ; text style; # (6.0) CLOCK FACE FIVE OCLOCK +1F554 FE0F ; emoji style; # (6.0) CLOCK FACE FIVE OCLOCK +1F555 FE0E ; text style; # (6.0) CLOCK FACE SIX OCLOCK +1F555 FE0F ; emoji style; # (6.0) CLOCK FACE SIX OCLOCK +1F556 FE0E ; text style; # (6.0) CLOCK FACE SEVEN OCLOCK +1F556 FE0F ; emoji style; # (6.0) CLOCK FACE SEVEN OCLOCK +1F557 FE0E ; text style; # (6.0) CLOCK FACE EIGHT OCLOCK +1F557 FE0F ; emoji style; # (6.0) CLOCK FACE EIGHT OCLOCK +1F558 FE0E ; text style; # (6.0) CLOCK FACE NINE OCLOCK +1F558 FE0F ; emoji style; # (6.0) CLOCK FACE NINE OCLOCK +1F559 FE0E ; text style; # (6.0) CLOCK FACE TEN OCLOCK +1F559 FE0F ; emoji style; # (6.0) CLOCK FACE TEN OCLOCK +1F55A FE0E ; text style; # (6.0) CLOCK FACE ELEVEN OCLOCK +1F55A FE0F ; emoji style; # (6.0) CLOCK FACE ELEVEN OCLOCK +1F55B FE0E ; text style; # (6.0) CLOCK FACE TWELVE OCLOCK +1F55B FE0F ; emoji style; # (6.0) CLOCK FACE TWELVE OCLOCK +1F55C FE0E ; text style; # (6.0) CLOCK FACE ONE-THIRTY +1F55C FE0F ; emoji style; # (6.0) CLOCK FACE ONE-THIRTY +1F55D FE0E ; text style; # (6.0) CLOCK FACE TWO-THIRTY +1F55D FE0F ; emoji style; # (6.0) CLOCK FACE TWO-THIRTY +1F55E FE0E ; text style; # (6.0) CLOCK FACE THREE-THIRTY +1F55E FE0F ; emoji style; # (6.0) CLOCK FACE THREE-THIRTY +1F55F FE0E ; text style; # (6.0) CLOCK FACE FOUR-THIRTY +1F55F FE0F ; emoji style; # (6.0) CLOCK FACE FOUR-THIRTY +1F560 FE0E ; text style; # (6.0) CLOCK FACE FIVE-THIRTY +1F560 FE0F ; emoji style; # (6.0) CLOCK FACE FIVE-THIRTY +1F561 FE0E ; text style; # (6.0) CLOCK FACE SIX-THIRTY +1F561 FE0F ; emoji style; # (6.0) CLOCK FACE SIX-THIRTY +1F562 FE0E ; text style; # (6.0) CLOCK FACE SEVEN-THIRTY +1F562 FE0F ; emoji style; # (6.0) CLOCK FACE SEVEN-THIRTY +1F563 FE0E ; text style; # (6.0) CLOCK FACE EIGHT-THIRTY +1F563 FE0F ; emoji style; # (6.0) CLOCK FACE EIGHT-THIRTY +1F564 FE0E ; text style; # (6.0) CLOCK FACE NINE-THIRTY +1F564 FE0F ; emoji style; # (6.0) CLOCK FACE NINE-THIRTY +1F565 FE0E ; text style; # (6.0) CLOCK FACE TEN-THIRTY +1F565 FE0F ; emoji style; # (6.0) CLOCK FACE TEN-THIRTY +1F566 FE0E ; text style; # (6.0) CLOCK FACE ELEVEN-THIRTY +1F566 FE0F ; emoji style; # (6.0) CLOCK FACE ELEVEN-THIRTY +1F567 FE0E ; text style; # (6.0) CLOCK FACE TWELVE-THIRTY +1F567 FE0F ; emoji style; # (6.0) CLOCK FACE TWELVE-THIRTY +1F56F FE0E ; text style; # (7.0) CANDLE +1F56F FE0F ; emoji style; # (7.0) CANDLE +1F570 FE0E ; text style; # (7.0) MANTELPIECE CLOCK +1F570 FE0F ; emoji style; # (7.0) MANTELPIECE CLOCK +1F573 FE0E ; text style; # (7.0) HOLE +1F573 FE0F ; emoji style; # (7.0) HOLE +1F574 FE0E ; text style; # (7.0) MAN IN BUSINESS SUIT LEVITATING +1F574 FE0F ; emoji style; # (7.0) MAN IN BUSINESS SUIT LEVITATING +1F575 FE0E ; text style; # (7.0) SLEUTH OR SPY +1F575 FE0F ; emoji style; # (7.0) SLEUTH OR SPY +1F576 FE0E ; text style; # (7.0) DARK SUNGLASSES +1F576 FE0F ; emoji style; # (7.0) DARK SUNGLASSES +1F577 FE0E ; text style; # (7.0) SPIDER +1F577 FE0F ; emoji style; # (7.0) SPIDER +1F578 FE0E ; text style; # (7.0) SPIDER WEB +1F578 FE0F ; emoji style; # (7.0) SPIDER WEB +1F579 FE0E ; text style; # (7.0) JOYSTICK +1F579 FE0F ; emoji style; # (7.0) JOYSTICK +1F587 FE0E ; text style; # (7.0) LINKED PAPERCLIPS +1F587 FE0F ; emoji style; # (7.0) LINKED PAPERCLIPS +1F58A FE0E ; text style; # (7.0) LOWER LEFT BALLPOINT PEN +1F58A FE0F ; emoji style; # (7.0) LOWER LEFT BALLPOINT PEN +1F58B FE0E ; text style; # (7.0) LOWER LEFT FOUNTAIN PEN +1F58B FE0F ; emoji style; # (7.0) LOWER LEFT FOUNTAIN PEN +1F58C FE0E ; text style; # (7.0) LOWER LEFT PAINTBRUSH +1F58C FE0F ; emoji style; # (7.0) LOWER LEFT PAINTBRUSH +1F58D FE0E ; text style; # (7.0) LOWER LEFT CRAYON +1F58D FE0F ; emoji style; # (7.0) LOWER LEFT CRAYON +1F590 FE0E ; text style; # (7.0) RAISED HAND WITH FINGERS SPLAYED +1F590 FE0F ; emoji style; # (7.0) RAISED HAND WITH FINGERS SPLAYED +1F5A5 FE0E ; text style; # (7.0) DESKTOP COMPUTER +1F5A5 FE0F ; emoji style; # (7.0) DESKTOP COMPUTER +1F5A8 FE0E ; text style; # (7.0) PRINTER +1F5A8 FE0F ; emoji style; # (7.0) PRINTER +1F5B1 FE0E ; text style; # (7.0) THREE BUTTON MOUSE +1F5B1 FE0F ; emoji style; # (7.0) THREE BUTTON MOUSE +1F5B2 FE0E ; text style; # (7.0) TRACKBALL +1F5B2 FE0F ; emoji style; # (7.0) TRACKBALL +1F5BC FE0E ; text style; # (7.0) FRAME WITH PICTURE +1F5BC FE0F ; emoji style; # (7.0) FRAME WITH PICTURE +1F5C2 FE0E ; text style; # (7.0) CARD INDEX DIVIDERS +1F5C2 FE0F ; emoji style; # (7.0) CARD INDEX DIVIDERS +1F5C3 FE0E ; text style; # (7.0) CARD FILE BOX +1F5C3 FE0F ; emoji style; # (7.0) CARD FILE BOX +1F5C4 FE0E ; text style; # (7.0) FILE CABINET +1F5C4 FE0F ; emoji style; # (7.0) FILE CABINET +1F5D1 FE0E ; text style; # (7.0) WASTEBASKET +1F5D1 FE0F ; emoji style; # (7.0) WASTEBASKET +1F5D2 FE0E ; text style; # (7.0) SPIRAL NOTE PAD +1F5D2 FE0F ; emoji style; # (7.0) SPIRAL NOTE PAD +1F5D3 FE0E ; text style; # (7.0) SPIRAL CALENDAR PAD +1F5D3 FE0F ; emoji style; # (7.0) SPIRAL CALENDAR PAD +1F5DC FE0E ; text style; # (7.0) COMPRESSION +1F5DC FE0F ; emoji style; # (7.0) COMPRESSION +1F5DD FE0E ; text style; # (7.0) OLD KEY +1F5DD FE0F ; emoji style; # (7.0) OLD KEY +1F5DE FE0E ; text style; # (7.0) ROLLED-UP NEWSPAPER +1F5DE FE0F ; emoji style; # (7.0) ROLLED-UP NEWSPAPER +1F5E1 FE0E ; text style; # (7.0) DAGGER KNIFE +1F5E1 FE0F ; emoji style; # (7.0) DAGGER KNIFE +1F5E3 FE0E ; text style; # (7.0) SPEAKING HEAD IN SILHOUETTE +1F5E3 FE0F ; emoji style; # (7.0) SPEAKING HEAD IN SILHOUETTE +1F5E8 FE0E ; text style; # (7.0) LEFT SPEECH BUBBLE +1F5E8 FE0F ; emoji style; # (7.0) LEFT SPEECH BUBBLE +1F5EF FE0E ; text style; # (7.0) RIGHT ANGER BUBBLE +1F5EF FE0F ; emoji style; # (7.0) RIGHT ANGER BUBBLE +1F5F3 FE0E ; text style; # (7.0) BALLOT BOX WITH BALLOT +1F5F3 FE0F ; emoji style; # (7.0) BALLOT BOX WITH BALLOT +1F5FA FE0E ; text style; # (7.0) WORLD MAP +1F5FA FE0F ; emoji style; # (7.0) WORLD MAP +1F610 FE0E ; text style; # (6.0) NEUTRAL FACE +1F610 FE0F ; emoji style; # (6.0) NEUTRAL FACE +1F687 FE0E ; text style; # (6.0) METRO +1F687 FE0F ; emoji style; # (6.0) METRO +1F68D FE0E ; text style; # (6.0) ONCOMING BUS +1F68D FE0F ; emoji style; # (6.0) ONCOMING BUS +1F691 FE0E ; text style; # (6.0) AMBULANCE +1F691 FE0F ; emoji style; # (6.0) AMBULANCE +1F694 FE0E ; text style; # (6.0) ONCOMING POLICE CAR +1F694 FE0F ; emoji style; # (6.0) ONCOMING POLICE CAR +1F698 FE0E ; text style; # (6.0) ONCOMING AUTOMOBILE +1F698 FE0F ; emoji style; # (6.0) ONCOMING AUTOMOBILE +1F6AD FE0E ; text style; # (6.0) NO SMOKING SYMBOL +1F6AD FE0F ; emoji style; # (6.0) NO SMOKING SYMBOL +1F6B2 FE0E ; text style; # (6.0) BICYCLE +1F6B2 FE0F ; emoji style; # (6.0) BICYCLE +1F6B9 FE0E ; text style; # (6.0) MENS SYMBOL +1F6B9 FE0F ; emoji style; # (6.0) MENS SYMBOL +1F6BA FE0E ; text style; # (6.0) WOMENS SYMBOL +1F6BA FE0F ; emoji style; # (6.0) WOMENS SYMBOL +1F6BC FE0E ; text style; # (6.0) BABY SYMBOL +1F6BC FE0F ; emoji style; # (6.0) BABY SYMBOL +1F6CB FE0E ; text style; # (7.0) COUCH AND LAMP +1F6CB FE0F ; emoji style; # (7.0) COUCH AND LAMP +1F6CD FE0E ; text style; # (7.0) SHOPPING BAGS +1F6CD FE0F ; emoji style; # (7.0) SHOPPING BAGS +1F6CE FE0E ; text style; # (7.0) BELLHOP BELL +1F6CE FE0F ; emoji style; # (7.0) BELLHOP BELL +1F6CF FE0E ; text style; # (7.0) BED +1F6CF FE0F ; emoji style; # (7.0) BED +1F6E0 FE0E ; text style; # (7.0) HAMMER AND WRENCH +1F6E0 FE0F ; emoji style; # (7.0) HAMMER AND WRENCH +1F6E1 FE0E ; text style; # (7.0) SHIELD +1F6E1 FE0F ; emoji style; # (7.0) SHIELD +1F6E2 FE0E ; text style; # (7.0) OIL DRUM +1F6E2 FE0F ; emoji style; # (7.0) OIL DRUM +1F6E3 FE0E ; text style; # (7.0) MOTORWAY +1F6E3 FE0F ; emoji style; # (7.0) MOTORWAY +1F6E4 FE0E ; text style; # (7.0) RAILWAY TRACK +1F6E4 FE0F ; emoji style; # (7.0) RAILWAY TRACK +1F6E5 FE0E ; text style; # (7.0) MOTOR BOAT +1F6E5 FE0F ; emoji style; # (7.0) MOTOR BOAT +1F6E9 FE0E ; text style; # (7.0) SMALL AIRPLANE +1F6E9 FE0F ; emoji style; # (7.0) SMALL AIRPLANE +1F6F0 FE0E ; text style; # (7.0) SATELLITE +1F6F0 FE0F ; emoji style; # (7.0) SATELLITE +1F6F3 FE0E ; text style; # (7.0) PASSENGER SHIP +1F6F3 FE0F ; emoji style; # (7.0) PASSENGER SHIP + +#Total sequences: 371 + +#EOF diff --git a/uri.c b/uri.c new file mode 100644 index 0000000..e09f7a9 --- /dev/null +++ b/uri.c @@ -0,0 +1,271 @@ +#include "uri.h" + +#include +#include +#include +#include + +#define LOG_MODULE "uri" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "debug.h" +#include "util.h" +#include "xmalloc.h" + +bool +uri_parse(const char *uri, size_t len, + char **scheme, char **user, char **password, char **host, + uint16_t *port, char **path, char **query, char **fragment) +{ + LOG_DBG("parse URI: \"%.*s\"", (int)len, uri); + + if (scheme != NULL) *scheme = NULL; + if (user != NULL) *user = NULL; + if (password != NULL) *password = NULL; + if (host != NULL) *host = NULL; + if (port != NULL) *port = 0; + if (path != NULL) *path = NULL; + if (query != NULL) *query = NULL; + if (fragment != NULL) *fragment = NULL; + + size_t left = len; + const char *start = uri; + const char *end = NULL; + + if ((end = memchr(start, ':', left)) == NULL) + goto err; + + size_t scheme_len = end - start; + if (scheme_len == 0) + goto err; + + if (scheme != NULL) + *scheme = xstrndup(start, scheme_len); + + LOG_DBG("scheme: \"%.*s\"", (int)scheme_len, start); + + start = end + 1; + left = len - (start - uri); + + /* Authinfo */ + if (left >= 2 && start[0] == '/' && start[1] == '/') { + start += 2; + left -= 2; + + /* [user[:password]@]@host[:port] */ + + /* Find beginning of path segment (required component + * following the authinfo) */ + const char *path_segment = memchr(start, '/', left); + if (path_segment == NULL) + goto err; + + size_t auth_left = path_segment - start; + + /* Do we have a user (and optionally a password)? */ + const char *user_pw_end = memchr(start, '@', auth_left); + if (user_pw_end != NULL) { + size_t user_pw_len = user_pw_end - start; + + /* Do we have a password? */ + const char *user_end = memchr(start, ':', user_pw_end - start); + if (user_end != NULL) { + size_t user_len = user_end - start; + if (user_len == 0) + goto err; + + if (user != NULL) + *user = xstrndup(start, user_len); + + const char *pw = user_end + 1; + size_t pw_len = user_pw_end - pw; + if (pw_len == 0) + goto err; + + if (password != NULL) + *password = xstrndup(pw, pw_len); + + LOG_DBG("user: \"%.*s\"", (int)user_len, start); + LOG_DBG("password: \"%.*s\"", (int)pw_len, pw); + } else { + size_t user_len = user_pw_end - start; + if (user_len == 0) + goto err; + + if (user != NULL) + *user = xstrndup(start, user_len); + + LOG_DBG("user: \"%.*s\"", (int)user_len, start); + } + + start = user_pw_end + 1; + left = len - (start - uri); + auth_left -= user_pw_len + 1; + } + + const char *host_end = memchr(start, ':', auth_left); + if (host_end != NULL) { + size_t host_len = host_end - start; + if (host != NULL) + *host = xstrndup(start, host_len); + + const char *port_str = host_end + 1; + size_t port_len = path_segment - port_str; + if (port_len == 0) + goto err; + + uint16_t _port = 0; + for (size_t i = 0; i < port_len; i++) { + if (!(port_str[i] >= '0' && port_str[i] <= '9')) + goto err; + + _port *= 10; + _port += port_str[i] - '0'; + } + + if (port != NULL) + *port = _port; + + LOG_DBG("host: \"%.*s\"", (int)host_len, start); + LOG_DBG("port: \"%.*s\" (%hu)", (int)port_len, port_str, _port); + } else { + size_t host_len = path_segment - start; + if (host != NULL) + *host = xstrndup(start, host_len); + + LOG_DBG("host: \"%.*s\"", (int)host_len, start); + } + + start = path_segment; + left = len - (start - uri); + } + + /* Do we have a query? */ + const char *query_start = memchr(start, '?', left); + const char *fragment_start = memchr(start, '#', left); + + if (streq(*scheme, "file")) { + /* Don't try to parse query/fragment in file URIs, just treat + the remaining text as path */ + query_start = NULL; + fragment_start = NULL; + } + + else if (query_start != NULL && fragment_start != NULL && + fragment_start < query_start) + { + /* Invalid URI - for now, ignore, and treat is as part of path */ + query_start = NULL; + fragment_start = NULL; + } + + size_t path_len = + query_start != NULL ? query_start - start : + fragment_start != NULL ? fragment_start - start : + left; + + if (path_len == 0) + goto err; + + /* Path - decode %xx encoded characters */ + if (path != NULL) { + const char *encoded = start; + char *decoded = xmalloc(path_len + 1); + char *p = decoded; + + size_t encoded_len = path_len; + size_t UNUSED decoded_len = 0; + + while (true) { + /* Find next '%' */ + const char *next = memchr(encoded, '%', encoded_len); + + if (next == NULL) { + strncpy(p, encoded, encoded_len); + decoded_len += encoded_len; + p += encoded_len; + break; + } + + /* Copy everything leading up to the '%' */ + size_t prefix_len = next - encoded; + memcpy(p, encoded, prefix_len); + + p += prefix_len; + encoded_len -= prefix_len; + decoded_len += prefix_len; + + if (hex2nibble(next[1]) <= 15 && hex2nibble(next[2]) <= 15) { + *p++ = hex2nibble(next[1]) << 4 | hex2nibble(next[2]); + decoded_len++; + encoded_len -= 3; + encoded = next + 3; + } else { + *p++ = *next; + decoded_len++; + encoded_len -= 1; + encoded = next + 1; + } + } + + *p = '\0'; + *path = decoded; + + LOG_DBG("path: encoded=\"%.*s\", decoded=\"%s\"", (int)path_len, start, decoded); + } else + LOG_DBG("path: encoded=\"%.*s\", decoded=", (int)path_len, start); + + start = query_start != NULL ? query_start + 1 : fragment_start != NULL ? fragment_start + 1 : uri + len; + left = len - (start - uri); + + if (query_start != NULL) { + size_t query_len = fragment_start != NULL + ? fragment_start - start : left; + + if (query_len == 0) + goto err; + + if (query != NULL) + *query = xstrndup(start, query_len); + + LOG_DBG("query: \"%.*s\"", (int)query_len, start); + + start = fragment_start != NULL ? fragment_start + 1 : uri + len; + left = len - (start - uri); + } + + if (fragment_start != NULL) { + if (left == 0) + goto err; + + if (fragment != NULL) + *fragment = xstrndup(start, left); + + LOG_DBG("fragment: \"%.*s\"", (int)left, start); + } + + return true; + +err: + if (scheme != NULL) free(*scheme); + if (user != NULL) free(*user); + if (password != NULL) free(*password); + if (host != NULL) free(*host); + if (path != NULL) free(*path); + if (query != NULL) free(*query); + if (fragment != NULL) free(*fragment); + return false; +} + +bool +hostname_is_localhost(const char *hostname) +{ + char this_host[_POSIX_HOST_NAME_MAX]; + if (gethostname(this_host, sizeof(this_host)) < 0) + this_host[0] = '\0'; + + return (hostname != NULL && ( + streq(hostname, "") || + streq(hostname, "localhost") || + streq(hostname, this_host))); +} diff --git a/uri.h b/uri.h new file mode 100644 index 0000000..b63290c --- /dev/null +++ b/uri.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include +#include + +bool uri_parse(const char *uri, size_t len, + char **scheme, char **user, char **password, char **host, + uint16_t *port, char **path, char **query, char **fragment); + +bool hostname_is_localhost(const char *hostname); diff --git a/url-mode.c b/url-mode.c new file mode 100644 index 0000000..44809f5 --- /dev/null +++ b/url-mode.c @@ -0,0 +1,830 @@ +#include "url-mode.h" + +#include +#include +#include +#include +#include + +#include +#include + +#define LOG_MODULE "url-mode" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "char32.h" +#include "grid.h" +#include "key-binding.h" +#include "quirks.h" +#include "render.h" +#include "selection.h" +#include "spawn.h" +#include "terminal.h" +#include "uri.h" +#include "util.h" +#include "xmalloc.h" + +static void url_destroy(struct url *url); + +static bool +execute_binding(struct seat *seat, struct terminal *term, + const struct key_binding *binding, uint32_t serial) +{ + const enum bind_action_url action = binding->action; + + switch (action) { + case BIND_ACTION_URL_NONE: + return false; + + case BIND_ACTION_URL_CANCEL: + urls_reset(term); + return true; + + case BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL: + term->urls_show_uri_on_jump_label = !term->urls_show_uri_on_jump_label; + render_refresh_urls(term); + return true; + + case BIND_ACTION_URL_COUNT: + return false; + + } + return true; +} + +static bool +spawn_url_launcher_with_token(struct terminal *term, + const char *url, + const char *xdg_activation_token) +{ + size_t argc; + char **argv; + + int dev_null = open("/dev/null", O_RDWR); + + if (dev_null < 0) { + LOG_ERRNO("failed to open /dev/null"); + return false; + } + + xassert(term->url_launch != NULL); + bool ret = false; + + if (spawn_expand_template( + term->url_launch, 2, + (const char *[]){"url", "match"}, + (const char *[]){url, url}, + &argc, &argv)) + { + ret = spawn( + term->reaper, term->cwd, argv, + dev_null, dev_null, dev_null, NULL, NULL, xdg_activation_token) >= 0; + + for (size_t i = 0; i < argc; i++) + free(argv[i]); + free(argv); + } + + close(dev_null); + return ret; +} + +struct spawn_activation_context { + struct terminal *term; + char *url; +}; + +static void +activation_token_done(const char *token, void *data) +{ + struct spawn_activation_context *ctx = data; + + spawn_url_launcher_with_token(ctx->term, ctx->url, token); + free(ctx->url); + free(ctx); +} + +static bool +spawn_url_launcher(struct seat *seat, struct terminal *term, const char *url, + uint32_t serial) +{ + xassert(term->url_launch != NULL); + + struct spawn_activation_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct spawn_activation_context){ + .term = term, + .url = xstrdup(url), + }; + + if (wayl_get_activation_token( + seat->wayl, seat, serial, term->window, &activation_token_done, ctx)) + { + /* Context free:d by callback */ + return true; + } + + free(ctx->url); + free(ctx); + + return spawn_url_launcher_with_token(term, url, NULL); +} + +static void +activate_url(struct seat *seat, struct terminal *term, const struct url *url, + uint32_t serial, bool paste_url_to_self) +{ + char *url_string = NULL; + + char *scheme, *host, *path; + if (uri_parse(url->url, strlen(url->url), &scheme, NULL, NULL, + &host, NULL, &path, NULL, NULL)) + { + if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { + /* + * This is a file in *this* computer. Pass only the + * filename to the URL-launcher. + * + * I.e. strip the ‘file://user@host/’ prefix. + */ + url_string = path; + } else + free(path); + + free(scheme); + free(host); + } + + if (url_string == NULL) + url_string = xstrdup(url->url); + + switch (url->action) { + case URL_ACTION_COPY: + if (paste_url_to_self) { + if (term->bracketed_paste) + term_to_slave(term, "\033[200~", 6); + + term_to_slave(term, url_string, strlen(url_string)); + + if (term->bracketed_paste) + term_to_slave(term, "\033[201~", 6); + } + if (text_to_clipboard(seat, term, url_string, seat->kbd.serial)) { + /* Now owned by our clipboard “manager” */ + url_string = NULL; + } + break; + + case URL_ACTION_LAUNCH: + case URL_ACTION_PERSISTENT: { + spawn_url_launcher(seat, term, url_string, serial); + break; + } + } + + free(url_string); +} + +void +urls_input(struct seat *seat, struct terminal *term, + const struct key_binding_set *bindings, uint32_t key, + xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, + const xkb_keysym_t *raw_syms, size_t raw_count, + uint32_t serial) +{ + /* + * Key bindings + */ + + /* Match untranslated symbols */ + tll_foreach(bindings->url, it) { + const struct key_binding *bind = &it->item; + if (bind->mods != mods || bind->mods == 0) + continue; + + for (size_t i = 0; i < raw_count; i++) { + if (bind->k.sym == raw_syms[i]) { + execute_binding(seat, term, bind, serial); + return; + } + } + } + + /* Match translated symbol */ + tll_foreach(bindings->url, it) { + const struct key_binding *bind = &it->item; + + if (bind->k.sym == sym && + bind->mods == (mods & ~consumed)) + { + execute_binding(seat, term, bind, serial); + return; + } + + } + + /* Match raw key code */ + tll_foreach(bindings->url, it) { + const struct key_binding *bind = &it->item; + if (bind->mods != mods || bind->mods == 0) + continue; + + /* Match raw key code */ + tll_foreach(bind->k.key_codes, code) { + if (code->item == key) { + execute_binding(seat, term, bind, serial); + return; + } + } + } + + size_t seq_len = c32len(term->url_keys); + + if (sym == XKB_KEY_BackSpace) { + if (seq_len > 0) { + term->url_keys[seq_len - 1] = U'\0'; + render_refresh_urls(term); + } + + return; + } + + if (mods & ~consumed) + return; + + char32_t wc = xkb_state_key_get_utf32(seat->kbd.xkb_state, key); + + /* + * Determine if this is a "valid" key. I.e. if there is a URL + * label with a key combo where this key is the next in + * sequence. + */ + + bool is_valid = false; + const struct url *match = NULL; + + tll_foreach(term->urls, it) { + if (it->item.key == NULL) + continue; + + const struct url *url = &it->item; + const size_t key_len = c32len(it->item.key); + + if (key_len >= seq_len + 1 && + c32ncasecmp(url->key, term->url_keys, seq_len) == 0 && + toc32lower(url->key[seq_len]) == toc32lower(wc)) + { + is_valid = true; + if (key_len == seq_len + 1) { + match = url; + break; + } + } + } + + if (match) { + // If the last hint character was uppercase, copy and paste + bool insert = term->conf->uppercase_regex_insert && wc == toc32upper(wc); + activate_url(seat, term, match, serial, insert); + + switch (match->action) { + case URL_ACTION_COPY: + case URL_ACTION_LAUNCH: + urls_reset(term); + break; + + case URL_ACTION_PERSISTENT: + term->url_keys[0] = U'\0'; + render_refresh_urls(term); + break; + } + } + + else if (is_valid) { + xassert(seq_len + 1 <= ALEN(term->url_keys)); + term->url_keys[seq_len] = wc; + render_refresh_urls(term); + } +} + +struct vline { + char *utf8; + size_t len; /* Length of utf8[] */ + size_t sz; /* utf8[] allocated size */ + struct coord *map; /* Maps utf8[ofs] to grid coordinates */ +}; + +static void +regex_detected(const struct terminal *term, enum url_action action, + const regex_t *preg, url_list_t *urls) +{ + /* + * Use regcomp()+regexec() to find patterns. + * + * Since we can't feed regexec() one character at a time, and + * since it doesn't accept wide characters, we need to build utf8 + * strings. + * + * Each string represents a logical line (i.e. handle line-wrap). + * To be able to map regex matches back to the grid, we store the + * grid coordinates of *each* character, in the line struct as + * well. This is offset based; utf8[ofs] has its grid coordinates + * in map[ofs. + */ + + /* There is *at most* term->rows logical lines */ + struct vline vlines[term->rows]; + size_t vline_idx = 0; + + memset(vlines, 0, sizeof(vlines)); + struct vline *vline = &vlines[vline_idx]; + + mbstate_t ps = {0}; + + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); + + for (int c = 0; c < term->cols; c++) { + const struct cell *cell = &row->cells[c]; + const char32_t *wc = &cell->wc; + size_t wc_count = 1; + + /* Expand combining characters */ + if (wc[0] >= CELL_COMB_CHARS_LO && wc[0] <= CELL_COMB_CHARS_HI) { + const struct composed *composed = + composed_lookup(term->composed, wc[0] - CELL_COMB_CHARS_LO); + xassert(composed != NULL); + + wc = composed->chars; + wc_count = composed->count; + } + + else if (wc[0] >= CELL_SPACER) + continue; + + /* Convert wide character to utf8 */ + for (size_t i = 0; i < wc_count; i++) { + char buf[16]; + size_t char_len = c32rtomb(buf, wc[i], &ps); + + if (char_len == (size_t)-1) + continue; + + + for (size_t j = 0; j < char_len; j++) { + const size_t requires_size = vline->len + char_len; + + /* Need to grow? Remember to save at least one byte for terminator */ + if (vline->sz == 0 || requires_size > vline->sz - 1) { + const size_t new_size = requires_size * 2; + vline->utf8 = xreallocarray(vline->utf8, new_size, 1); + vline->map = xreallocarray(vline->map, new_size, sizeof(vline->map[0])); + vline->sz = new_size; + } + + vline->utf8[vline->len + j] = + (buf[j] == '\0') ? ' ' : buf[j]; + vline->map[vline->len + j] = (struct coord){c, term->grid->view + r}; + } + + vline->len += char_len; + } + } + + if (row->linebreak) { + if (vline->len > 0) { + vline->utf8[vline->len++] = '\0'; + ps = (mbstate_t){0}; + + vline_idx++; + vline = &vlines[vline_idx]; + } + } + } + + /* Terminate the last line, if necessary */ + if (vline_idx < ALEN(vlines) && + vline->len > 0 && vline->utf8[vline->len - 1] != '\0') + { + vline->utf8[vline->len++] = '\0'; + } + + for (size_t i = 0; i < ALEN(vlines); i++) { + const struct vline *v = &vlines[i]; + if (v->utf8 == NULL) + continue; + + const char *search_string = v->utf8; + while (true) { + regmatch_t matches[preg->re_nsub + 1]; + int r = regexec(preg, search_string, preg->re_nsub + 1, matches, 0); + + if (r == REG_NOMATCH) + break; + + const size_t mlen = matches[1].rm_eo - matches[1].rm_so; + const size_t start = &search_string[matches[1].rm_so] - v->utf8; + const size_t end = start + mlen; + + LOG_DBG( + "regex match at row %d: %.*s (%zu bytes), row/col = %dx%d", + matches[1].rm_so, (int)mlen, &search_string[matches[1].rm_so], + mlen, v->map[start].row, v->map[start].col); + + tll_push_back( + *urls, + ((struct url){ + .id = (uint64_t)rand() << 32 | rand(), + .url = xstrndup(&v->utf8[start], mlen), + .range = { + .start = v->map[start], + .end = v->map[end - 1], /* Inclusive */ + }, + .action = action, + .osc8 = false})); + + search_string += matches[0].rm_eo; + } + + free(v->utf8); + free(v->map); + } +} + +static void +osc8_uris(const struct terminal *term, enum url_action action, url_list_t *urls) +{ + bool dont_touch_url_attr = false; + + switch (term->conf->url.osc8_underline) { + case OSC8_UNDERLINE_URL_MODE: + dont_touch_url_attr = false; + break; + + case OSC8_UNDERLINE_ALWAYS: + dont_touch_url_attr = true; + break; + } + + for (int r = 0; r < term->rows; r++) { + const struct row *row = grid_row_in_view(term->grid, r); + const struct row_data *extra = row->extra; + + if (extra == NULL) + continue; + + for (size_t i = 0; i < extra->uri_ranges.count; i++) { + const struct row_range *range = &extra->uri_ranges.v[i]; + + struct coord start = { + .col = range->start, + .row = r + term->grid->view, + }; + struct coord end = { + .col = range->end, + .row = r + term->grid->view, + }; + tll_push_back( + *urls, + ((struct url){ + .id = range->uri.id, + .url = xstrdup(range->uri.uri), + .range = { + .start = start, + .end = end, + }, + .action = action, + .url_mode_dont_change_url_attr = dont_touch_url_attr, + .osc8 = true})); + } + } +} + +static void +remove_overlapping(url_list_t *urls, int cols) +{ + tll_foreach(*urls, outer) { + tll_foreach(*urls, inner) { + if (outer == inner) + continue; + + const struct url *out = &outer->item; + const struct url *in = &inner->item; + + uint64_t in_start = in->range.start.row * cols + in->range.start.col; + uint64_t in_end = in->range.end.row * cols + in->range.end.col; + + uint64_t out_start = out->range.start.row * cols + out->range.start.col; + uint64_t out_end = out->range.end.row * cols + out->range.end.col; + + if ((in_start <= out_start && in_end >= out_start) || + (in_start <= out_end && in_end >= out_end) || + (in_start >= out_start && in_end <= out_end)) + { + /* + * OSC-8 URLs can't overlap with each + * other. + * + * Similarly, auto-detected URLs cannot overlap with + * each other. + * + * But OSC-8 URLs can overlap with auto-detected ones. + */ + xassert(in->osc8 || out->osc8); + + if (in->osc8) + outer->item.duplicate = true; + else + inner->item.duplicate = true; + } + } + } + + tll_foreach(*urls, it) { + if (it->item.duplicate) { + url_destroy(&it->item); + tll_remove(*urls, it); + } + } +} + +void +urls_collect(const struct terminal *term, enum url_action action, + const regex_t *preg, bool osc8, url_list_t *urls) +{ + xassert(tll_length(term->urls) == 0); + if (osc8) + osc8_uris(term, action, urls); + regex_detected(term, action, preg, urls); + remove_overlapping(urls, term->grid->num_cols); +} + +static void +generate_key_combos(const struct config *conf, + size_t count, char32_t *combos[static count]) +{ + const char32_t *alphabet = conf->url.label_letters; + const size_t alphabet_len = c32len(alphabet); + + size_t hints_count = 1; + char32_t **hints = xmalloc(hints_count * sizeof(hints[0])); + + hints[0] = xc32dup(U""); + + size_t offset = 0; + do { + const char32_t *prefix = hints[offset++]; + const size_t prefix_len = c32len(prefix); + + hints = xrealloc(hints, (hints_count + alphabet_len) * sizeof(hints[0])); + + const char32_t *wc = &alphabet[0]; + for (size_t i = 0; i < alphabet_len; i++, wc++) { + char32_t *hint = xmalloc((prefix_len + 1 + 1) * sizeof(char32_t)); + hints[hints_count + i] = hint; + + /* Will be reversed later */ + hint[0] = *wc; + c32cpy(&hint[1], prefix); + } + hints_count += alphabet_len; + } while (hints_count - offset < count); + + xassert(hints_count - offset >= count); + + /* Copy slice of 'hints' array to the caller provided array */ + for (size_t i = 0; i < hints_count; i++) { + if (i >= offset && i < offset + count) + combos[i - offset] = hints[i]; + else + free(hints[i]); + } + free(hints); + + /* Reverse all strings */ + for (size_t i = 0; i < count; i++) { + const size_t len = c32len(combos[i]); + for (size_t j = 0; j < len / 2; j++) { + char32_t tmp = combos[i][j]; + combos[i][j] = combos[i][len - j - 1]; + combos[i][len - j - 1] = tmp; + } + } +} + +void +urls_assign_key_combos(const struct config *conf, url_list_t *urls) +{ + const size_t count = tll_length(*urls); + if (count == 0) + return; + + char32_t *combos[count]; + generate_key_combos(conf, count, combos); + + size_t combo_idx = 0; + + tll_rforeach(*urls, it) { + bool id_already_seen = false; + + /* Look for already processed URLs where both the URI and the + * ID matches */ + tll_rforeach(*urls, it2) { + if (&it->item == &it2->item) + break; + + if (it->item.id == it2->item.id && + streq(it->item.url, it2->item.url)) + { + id_already_seen = true; + break; + } + } + + if (id_already_seen) + continue; + + /* + * Scan previous URLs, and check if *this* URL matches any of + * them; if so, reuse the *same* key combo. + */ + bool url_already_seen = false; + tll_rforeach(*urls, it2) { + if (&it->item == &it2->item) + break; + + if (streq(it->item.url, it2->item.url)) { + it->item.key = xc32dup(it2->item.key); + url_already_seen = true; + break; + } + } + + if (!url_already_seen) + it->item.key = combos[combo_idx++]; + } + + /* Free combos we didn't use up */ + for (size_t i = combo_idx; i < count; i++) + free(combos[i]); + +#if defined(_DEBUG) && LOG_ENABLE_DBG + tll_rforeach(*urls, it) { + if (it->item.key == NULL) + continue; + + char *key = ac32tombs(it->item.key); + xassert(key != NULL); + + LOG_DBG("URL: %s (key=%s, id=%"PRIu64")", it->item.url, key, it->item.id); + free(key); + } +#endif +} + +static void +tag_cells_for_url(struct terminal *term, const struct url *url, bool value) +{ + if (url->url_mode_dont_change_url_attr) + return; + + struct grid *grid = term->url_grid_snapshot; + xassert(grid != NULL); + + const struct coord *start = &url->range.start; + const struct coord *end = &url->range.end; + + size_t end_r = end->row & (grid->num_rows - 1); + + size_t r = start->row & (grid->num_rows - 1); + size_t c = start->col; + + struct row *row = grid->rows[r]; + row->dirty = true; + + while (true) { + struct cell *cell = &row->cells[c]; + cell->attrs.url = value; + cell->attrs.clean = 0; + + if (r == end_r && c == end->col) + break; + + if (++c >= term->cols) { + r = (r + 1) & (grid->num_rows - 1); + c = 0; + + row = grid->rows[r]; + if (row == NULL) { + /* Un-allocated scrollback. This most likely means a + * runaway OSC-8 URL. */ + break; + } + row->dirty = true; + } + } +} + +void +urls_render(struct terminal *term, const struct config_spawn_template *launch) +{ + struct wl_window *win = term->window; + + if (tll_length(win->term->urls) == 0) + return; + + /* Disable IME while in URL-mode */ + if (term_ime_is_enabled(term)) { + term->ime_reenable_after_url_mode = true; + term_ime_disable(term); + } + + /* Dirty the last cursor, to ensure it is erased */ + { + struct row *cursor_row = term->render.last_cursor.row; + if (cursor_row != NULL) { + struct cell *cell = &cursor_row->cells[term->render.last_cursor.col]; + cell->attrs.clean = 0; + cursor_row->dirty = true; + } + } + term->render.last_cursor.row = NULL; + + /* Clear scroll damage, to ensure we don't apply it twice (once on + * the snapshot:ed grid, and then later again on the real grid) */ + tll_free(term->grid->scroll_damage); + + /* Damage the entire view, to ensure a full screen redraw, both + * now, when entering URL mode, and later, when exiting it. */ + term_damage_view(term); + + /* Snapshot the current grid */ + term->url_grid_snapshot = grid_snapshot(term->grid); + + /* Remember which launcher to use */ + term->url_launch = launch; + + xassert(tll_length(win->urls) == 0); + tll_foreach(win->term->urls, it) { + struct wl_url url = {.url = &it->item}; + wayl_win_subsurface_new(win, &url.surf, false); + + tll_push_back(win->urls, url); + tag_cells_for_url(term, &it->item, true); + } + + render_refresh_urls(term); + render_refresh(term); +} + +static void +url_destroy(struct url *url) +{ + free(url->url); + free(url->key); +} + +void +urls_reset(struct terminal *term) +{ + if (likely(tll_length(term->urls) == 0)) { + xassert(term->url_grid_snapshot == NULL); + return; + } + + grid_free(term->url_grid_snapshot); + free(term->url_grid_snapshot); + term->url_grid_snapshot = NULL; + + /* + * Make sure "last cursor" doesn't point to a row in the just + * free:d snapshot grid. + * + * Note that it will still be erased properly (if hasn't already), + * since we marked the cell as dirty *before* taking the grid + * snapshot. + */ + term->render.last_cursor.row = NULL; + + if (term->window != NULL) { + tll_foreach(term->window->urls, it) { + wayl_win_subsurface_destroy(&it->item.surf); + tll_remove(term->window->urls, it); + } + } + + tll_foreach(term->urls, it) { + url_destroy(&it->item); + tll_remove(term->urls, it); + } + + term->urls_show_uri_on_jump_label = false; + memset(term->url_keys, 0, sizeof(term->url_keys)); + + /* Re-enable IME, if it was enabled before we entered URL-mode */ + if (term->ime_reenable_after_url_mode) { + term->ime_reenable_after_url_mode = false; + term_ime_enable(term); + } + + render_refresh(term); +} diff --git a/url-mode.h b/url-mode.h new file mode 100644 index 0000000..758cd92 --- /dev/null +++ b/url-mode.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +#include "config.h" +#include "key-binding.h" +#include "terminal.h" + +static inline bool urls_mode_is_active(const struct terminal *term) +{ + return tll_length(term->urls) > 0; +} + +void urls_collect( + const struct terminal *term, enum url_action action, const regex_t *preg, + bool osc8, url_list_t *urls); +void urls_assign_key_combos(const struct config *conf, url_list_t *urls); + +void urls_render(struct terminal *term, const struct config_spawn_template *launch); +void urls_reset(struct terminal *term); + +void urls_input(struct seat *seat, struct terminal *term, + const struct key_binding_set *bindings, uint32_t key, + xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, + const xkb_keysym_t *raw_syms, size_t raw_count, + uint32_t serial); diff --git a/user-notification.c b/user-notification.c new file mode 100644 index 0000000..6c1157d --- /dev/null +++ b/user-notification.c @@ -0,0 +1,14 @@ +#include "user-notification.h" +#include +#include "xmalloc.h" + +void +user_notification_add_fmt(user_notifications_t *notifications, + enum user_notification_kind kind, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + char *text = xvasprintf(fmt, ap); + va_end(ap); + user_notification_add(notifications, kind, text); +} diff --git a/user-notification.h b/user-notification.h new file mode 100644 index 0000000..df2390c --- /dev/null +++ b/user-notification.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "macros.h" + +enum user_notification_kind { + USER_NOTIFICATION_DEPRECATED, + USER_NOTIFICATION_WARNING, + USER_NOTIFICATION_ERROR, +}; + +struct user_notification { + enum user_notification_kind kind; + char *text; +}; + +typedef tll(struct user_notification) user_notifications_t; + +static inline void +user_notifications_free(user_notifications_t *notifications) +{ + tll_foreach(*notifications, it) + free(it->item.text); + tll_free(*notifications); +} + +static inline void +user_notification_add(user_notifications_t *notifications, + enum user_notification_kind kind, char *text) +{ + struct user_notification notification = { + .kind = kind, + .text = text + }; + tll_push_back(*notifications, notification); +} + +void user_notification_add_fmt(user_notifications_t *notifications, + enum user_notification_kind kind, + const char *fmt, ...) PRINTF(3); diff --git a/util.h b/util.h new file mode 100644 index 0000000..3746e26 --- /dev/null +++ b/util.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include + +#define ALEN(v) (sizeof(v) / sizeof((v)[0])) +#define min(x, y) ((x) < (y) ? (x) : (y)) +#define max(x, y) ((x) > (y) ? (x) : (y)) + +static inline bool +streq(const char *a, const char *b) +{ + return strcmp(a, b) == 0; +} + +static inline const char * +thrd_err_as_string(int thrd_err) +{ + switch (thrd_err) { + case thrd_success: return "success"; + case thrd_busy: return "busy"; + case thrd_nomem: return "no memory"; + case thrd_timedout: return "timedout"; + + case thrd_error: + default: return "unknown error"; + } + + return "unknown error"; +} + +static inline uint64_t +sdbm_hash(const char *s) +{ + uint64_t hash = 0; + + for (; *s != '\0'; s++) { + int c = *s; + hash = c + (hash << 6) + (hash << 16) - hash; + } + + return hash; +} + +enum { + HEX_DIGIT_INVALID = 16 +}; + +static inline uint8_t +hex2nibble(char c) +{ + switch (c) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + return c - '0'; + + case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': + return c - 'a' + 10; + + case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': + return c - 'A' + 10; + } + + return HEX_DIGIT_INVALID; +} diff --git a/utils/meson.build b/utils/meson.build new file mode 100644 index 0000000..2836788 --- /dev/null +++ b/utils/meson.build @@ -0,0 +1 @@ +executable('xtgettcap', 'xtgettcap.c') diff --git a/utils/xtgettcap.c b/utils/xtgettcap.c new file mode 100644 index 0000000..82ee008 --- /dev/null +++ b/utils/xtgettcap.c @@ -0,0 +1,199 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static struct termios orig_termios; + +static void +disable_raw_mode(void) +{ + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) < 0) + exit(__LINE__); +} + +static void +enable_raw_mode(void) +{ + if (tcgetattr(STDIN_FILENO, &orig_termios) < 0) + exit(__LINE__); + + atexit(disable_raw_mode); + + struct termios raw = orig_termios; + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + raw.c_oflag &= ~(OPOST); + raw.c_cflag |= (CS8); + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 1; + + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) < 0) + exit(__LINE__); +} + +static const char * +hexlify(const char *s) +{ + static char buf[1024]; + + const size_t len = strlen(s); + for (size_t i = 0; i < len; i++) + sprintf(&buf[i * 2], "%02x", s[i]); + buf[len * 2 + 1] = '\0'; + + return buf; +} + +static size_t +unhexlify(char *dst, const char *src) +{ + size_t count = 0; + for (const char *p = src; *p != '\0'; p += 2, dst++, count++) + sscanf(p, "%02hhx", (unsigned char *)dst); + + *dst = '\0'; + return count; +} + +int +main(int argc, const char *const *argv) +{ + const size_t query_count = argc - 1; + + if (query_count == 0) + return 0; + + enable_raw_mode(); + + printf("\x1bP+q"); + for (int i = 1; i < argc; i++) + printf("%s%s", i > 1 ? ";" : "", hexlify(argv[i])); + printf("\033\\"); + + fflush(NULL); + + size_t replies = 0; + while (replies < query_count) { + struct pollfd fds[] = {{.fd = STDIN_FILENO, .events = POLLIN}}; + int r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1); + if (r < 0) + exit(__LINE__); + + char buf[1024] = {0}; + ssize_t count = read(STDIN_FILENO, buf, sizeof(buf)); + + if (count < 0) + exit(__LINE__); + + if (count == 1 && buf[0] == 'q') + break; + + printf("reply: (%zd chars): ", count); + + for (size_t i = 0; i < (size_t)count; i++) { + if (isprint(buf[i])) + printf("%c", buf[i]); + else if (buf[i] == '\033') + printf("\033[1;31m\033[m"); + else + printf("%02x", (uint8_t)buf[i]); + } + printf("\r\n"); + + const char *p = buf; + const char *end = buf + count; + + while (p < end) { + + const char *ST = strstr(p, "\033\\"); + if (ST == NULL) + break; + + if (count < 5 || + (strncmp(p, "\033P1+r", 5) != 00 && + strncmp(p, "\033P0+r", 5) != 0)) + { + break; + } + + const bool success = p[2] == '1'; + + char decoded[1024]; + char copy[ST - &p[5] + 1]; + strncpy(copy, &p[5], ST - &p[5]); + copy[ST - &p[5]] = '\0'; + + char *saveptr = NULL; + for (char *key_value = strtok_r(copy, "; ", &saveptr); + key_value != NULL; + key_value = strtok_r(NULL, "; ", &saveptr)) + { + // printf("key-value=%s\n", key_value); + const char *key = strtok(key_value, "="); + const char *value = strtok(NULL, "="); + + if (key == NULL) + continue; + +#if 0 + assert((success && value != NULL) || + (!success && value == NULL)); +#endif + + //printf("key=%s, value=%s\n", key, value); + size_t len = unhexlify(decoded, key); + + if (value != NULL) { + decoded[len++] = '='; + len += unhexlify(&decoded[len], value); + } + + const int color = success ? 39 : 31; + + printf(" \033[%dm", color); + for (size_t i = 0 ; i < len; i++) { + if (isprint(decoded[i])) { + /* All printable characters */ + printf("%c", decoded[i]); + } + + else if (decoded[i] == '\033') { + /* ESC */ + printf("\033[1;31m\033[22;%dm", color); + } + + else if (decoded[i] >= '\x00' && decoded[i] <= '\x5f') { + /* Control characters, e.g. ^G etc */ + printf("\033[1m^%c\033[22m", decoded[i] + '@'); + } + + else if (decoded[i] == '\x7f') { + /* Control character ^? */ + printf("\033[1m^?\033[22m"); + } + + else { + /* Unknown: print hex representation */ + printf("\033[1m%02x\033[22m", (uint8_t)decoded[i]); + } + } + printf("\033[m\r\n"); + replies++; + } + + p = ST + 2; + } + + } + + return 0; +} diff --git a/vt.c b/vt.c new file mode 100644 index 0000000..1d8297b --- /dev/null +++ b/vt.c @@ -0,0 +1,1133 @@ +#include "vt.h" + +#include +#include +#include + +#if defined(FOOT_GRAPHEME_CLUSTERING) + #include +#endif + +#define LOG_MODULE "vt" +#define LOG_ENABLE_DBG 0 +#include "log.h" +#include "char32.h" +#include "config.h" +#include "csi.h" +#include "dcs.h" +#include "debug.h" +#include "osc.h" +#include "sixel.h" +#include "util.h" +#include "xmalloc.h" + +#define UNHANDLED() LOG_DBG("unhandled: %s", esc_as_string(term, final)) + +/* https://vt100.net/emu/dec_ansi_parser */ + +enum state { + STATE_GROUND, + STATE_ESCAPE, + STATE_ESCAPE_INTERMEDIATE, + + STATE_CSI_ENTRY, + STATE_CSI_PARAM, + STATE_CSI_INTERMEDIATE, + STATE_CSI_IGNORE, + + STATE_OSC_STRING, + + STATE_DCS_ENTRY, + STATE_DCS_PARAM, + STATE_DCS_INTERMEDIATE, + STATE_DCS_IGNORE, + STATE_DCS_PASSTHROUGH, + + STATE_SOS_PM_APC_STRING, + + STATE_UTF8_21, + STATE_UTF8_31, + STATE_UTF8_32, + STATE_UTF8_41, + STATE_UTF8_42, + STATE_UTF8_43, +}; + +#if defined(_DEBUG) && defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG && 0 +static const char *const state_names[] = { + [STATE_GROUND] = "ground", + + [STATE_ESCAPE] = "escape", + [STATE_ESCAPE_INTERMEDIATE] = "escape intermediate", + + [STATE_CSI_ENTRY] = "CSI entry", + [STATE_CSI_PARAM] = "CSI param", + [STATE_CSI_INTERMEDIATE] = "CSI intermediate", + [STATE_CSI_IGNORE] = "CSI ignore", + + [STATE_OSC_STRING] = "OSC string", + + [STATE_DCS_ENTRY] = "DCS entry", + [STATE_DCS_PARAM] = "DCS param", + [STATE_DCS_INTERMEDIATE] = "DCS intermediate", + [STATE_DCS_IGNORE] = "DCS ignore", + [STATE_DCS_PASSTHROUGH] = "DCS passthrough", + + [STATE_SOS_PM_APC_STRING] = "sos/pm/apc string", + + [STATE_UTF8_21] = "UTF8 2-byte 1/2", + [STATE_UTF8_31] = "UTF8 3-byte 1/3", + [STATE_UTF8_32] = "UTF8 3-byte 2/3", +}; +#endif + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG +static const char * +esc_as_string(struct terminal *term, uint8_t final) +{ + static char msg[1024]; + int c = snprintf(msg, sizeof(msg), "\\E"); + + for (size_t i = 0; i < sizeof(term->vt.private); i++) { + char value = (term->vt.private >> (i * 8)) & 0xff; + if (value == 0) + break; + c += snprintf(&msg[c], sizeof(msg) - c, "%c", value); + } + + xassert(term->vt.params.idx == 0); + + snprintf(&msg[c], sizeof(msg) - c, "%c", final); + return msg; + +} +#endif + +static void +action_ignore(struct terminal *term) +{ +} + +static void +action_clear(struct terminal *term) +{ + term->vt.params.idx = 0; + term->vt.private = 0; +} + +static void +action_execute(struct terminal *term, uint8_t c) +{ + LOG_DBG("execute: 0x%02x", c); + switch (c) { + + /* + * 7-bit C0 control characters + */ + + case '\0': + break; + + case '\a': + /* BEL - bell */ + term_bell(term); + break; + + case '\b': + /* backspace */ +#if 0 + /* + * This is the "correct" BS behavior. However, it doesn't play + * nicely with bw/auto_left_margin, hence the alternative + * implementation below. + * + * Note that it breaks vttest "1. Test of cursor movements -> + * Test of autowrap" + */ + term_cursor_left(term, 1); +#else + if (term->grid->cursor.lcf) + term->grid->cursor.lcf = false; + else { + /* Reverse wrap */ + if (unlikely(term->grid->cursor.point.col == 0) && + likely(term->reverse_wrap && term->auto_margin)) + { + if (term->grid->cursor.point.row <= term->scroll_region.start) { + /* Don't wrap past, or inside, the scrolling region(?) */ + } else + term_cursor_to( + term, + term->grid->cursor.point.row - 1, + term->cols - 1); + } else + term_cursor_left(term, 1); + } +#endif + break; + + case '\t': { + /* HT - horizontal tab */ + int start_col = term->grid->cursor.point.col; + int new_col = term->cols - 1; + + tll_foreach(term->tab_stops, it) { + if (it->item > start_col) { + new_col = it->item; + break; + } + } + xassert(new_col >= start_col); + xassert(new_col < term->cols); + + struct row *row = term->grid->cur_row; + + bool emit_tab_char = (row->cells[start_col].wc == 0 || + row->cells[start_col].wc == U' '); + + /* Check if all cells from here until the next tab stop are empty */ + for (const struct cell *cell = &row->cells[start_col + 1]; + cell < &row->cells[new_col]; + cell++) + { + if (!(cell->wc == 0 || cell->wc == U' ')) { + emit_tab_char = false; + break; + } + } + + /* + * Emit a tab in current cell, and write spaces to the + * subsequent cells, all the way until the next tab stop. + */ + if (emit_tab_char) { + row->dirty = true; + + row->cells[start_col].wc = U'\t'; + row->cells[start_col].attrs.clean = 0; + + for (struct cell *cell = &row->cells[start_col + 1]; + cell < &row->cells[new_col]; + cell++) + { + cell->wc = U' '; + cell->attrs.clean = 0; + } + } + + /* According to the specification, HT _should_ cancel LCF. But + * XTerm, and nearly all other emulators, don't. So we follow + * suit */ + bool lcf = term->grid->cursor.lcf; + term_cursor_right(term, new_col - start_col); + term->grid->cursor.lcf = lcf; + break; + } + + case '\n': + case '\v': + case '\f': + /* LF - \n - line feed */ + /* VT - \v - vertical tab */ + /* FF - \f - form feed */ + term_linefeed(term); + break; + + case '\r': + /* CR - carriage ret */ + term_carriage_return(term); + break; + + case '\x0e': + /* SO - shift out */ + term->charsets.selected = G1; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); + break; + + case '\x0f': + /* SI - shift in */ + term->charsets.selected = G0; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); + break; + + /* + * 8-bit C1 control characters + * + * We ignore these, but keep them here for reference, along + * with their corresponding 7-bit variants. + * + * As far as I can tell, XTerm also ignores these _when in + * UTF-8 mode_. Which would be the normal mode of operation + * these days. And since we _only_ support UTF-8... + */ + +#if 0 + case '\x84': /* IND -> ESC D */ + case '\x85': /* NEL -> ESC E */ + case '\x88': /* Tab Set -> ESC H */ + case '\x8d': /* RI -> ESC M */ + case '\x8e': /* SS2 -> ESC N */ + case '\x8f': /* SS3 -> ESC O */ + case '\x90': /* DCS -> ESC P */ + case '\x96': /* SPA -> ESC V */ + case '\x97': /* EPA -> ESC W */ + case '\x98': /* SOS -> ESC X */ + case '\x9a': /* DECID -> ESC Z (obsolete form of CSI c) */ + case '\x9b': /* CSI -> ESC [ */ + case '\x9c': /* ST -> ESC \ */ + case '\x9d': /* OSC -> ESC ] */ + case '\x9e': /* PM -> ESC ^ */ + case '\x9f': /* APC -> ESC _ */ + break; +#endif + + default: + break; + } +} + +static void +action_print(struct terminal *term, uint8_t c) +{ + term_reset_grapheme_state(term); + term->ascii_printer(term, c); +} + +static void +action_param_lazy_init(struct terminal *term) +{ + if (term->vt.params.idx == 0) { + struct vt_param *param = &term->vt.params.v[0]; + + term->vt.params.cur = param; + param->value = 0; + param->sub.idx = 0; + param->sub.cur = NULL; + term->vt.params.idx = 1; + } +} + +static void +action_param_new(struct terminal *term, uint8_t c) +{ + xassert(c == ';'); + action_param_lazy_init(term); + + const size_t max_params + = sizeof(term->vt.params.v) / sizeof(term->vt.params.v[0]); + + struct vt_param *param; + + if (unlikely(term->vt.params.idx >= max_params)) { + static bool have_warned = false; + if (!have_warned) { + have_warned = true; + LOG_WARN( + "unsupported: escape with more than %zu parameters " + "(will not warn again)", + sizeof(term->vt.params.v) / sizeof(term->vt.params.v[0])); + } + param = &term->vt.params.dummy; + } else + param = &term->vt.params.v[term->vt.params.idx++]; + + term->vt.params.cur = param; + param->value = 0; + param->sub.idx = 0; + param->sub.cur = NULL; +} + +static void +action_param_new_subparam(struct terminal *term, uint8_t c) +{ + xassert(c == ':'); + action_param_lazy_init(term); + + const size_t max_sub_params + = sizeof(term->vt.params.v[0].sub.value) / sizeof(term->vt.params.v[0].sub.value[0]); + + struct vt_param *param = term->vt.params.cur; + unsigned *sub_param_value; + + if (unlikely(param->sub.idx >= max_sub_params)) { + static bool have_warned = false; + if (!have_warned) { + have_warned = true; + LOG_WARN( + "unsupported: escape with more than %zu sub-parameters " + "(will not warn again)", + sizeof(term->vt.params.v[0].sub.value) / sizeof(term->vt.params.v[0].sub.value[0])); + } + + sub_param_value = ¶m->sub.dummy; + } else + sub_param_value = ¶m->sub.value[param->sub.idx++]; + + param->sub.cur = sub_param_value; + *sub_param_value = 0; +} + +static void +action_param(struct terminal *term, uint8_t c) +{ + action_param_lazy_init(term); + xassert(term->vt.params.cur != NULL); + + struct vt_param *param = term->vt.params.cur; + unsigned *value; + + if (unlikely(param->sub.cur != NULL)) + value = param->sub.cur; + else + value = ¶m->value; + + unsigned v = *value; + v *= 10; + v += c - '0'; + *value = v; +} + +static void +action_collect(struct terminal *term, uint8_t c) +{ + LOG_DBG("collect: %c", c); + + /* + * Having more than one private is *very* rare. Foot only supports + * a *single* escape with two privates, and none with three or + * more. + * + * As such, we optimize *reading* the private(s), and *resetting* + * them (in action_clear()). Writing is ok if it's a bit slow. + */ + + if ((term->vt.private & 0xff) == 0) + term->vt.private = c; + else if (((term->vt.private >> 8) & 0xff) == 0) + term->vt.private |= c << 8; + else if (((term->vt.private >> 16) & 0xff) == 0) + term->vt.private |= c << 16; + else if (((term->vt.private >> 24) & 0xff) == 0) + term->vt.private |= c << 24; + else + LOG_WARN("only four private/intermediate characters supported"); +} + +UNITTEST +{ + struct terminal term = {.vt = {.private = 0}}; + uint32_t expected = ' '; + action_collect(&term, ' '); + xassert(term.vt.private == expected); + + expected |= '/' << 8; + action_collect(&term, '/'); + xassert(term.vt.private == expected); + + expected |= '<' << 16; + action_collect(&term, '<'); + xassert(term.vt.private == expected); + + expected |= '?' << 24; + action_collect(&term, '?'); + xassert(term.vt.private == expected); + + action_collect(&term, '?'); + xassert(term.vt.private == expected); +} + +static void +tab_set(struct terminal *term) +{ + int col = term->grid->cursor.point.col; + + if (tll_length(term->tab_stops) == 0 || tll_back(term->tab_stops) < col) { + tll_push_back(term->tab_stops, col); + return; + } + + tll_foreach(term->tab_stops, it) { + if (it->item < col) { + continue; + } + if (it->item > col) { + tll_insert_before(term->tab_stops, it, col); + } + break; + } +} + +static void +action_esc_dispatch(struct terminal *term, uint8_t final) +{ + LOG_DBG("ESC: %s", esc_as_string(term, final)); + + switch (term->vt.private) { + case 0: + switch (final) { + case '7': + term_save_cursor(term); + break; + + case '8': + term_restore_cursor(term, &term->grid->saved_cursor); + break; + + case 'c': + term_reset(term, true); + break; + + case 'n': + /* LS2 - Locking Shift 2 */ + term->charsets.selected = G2; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); + break; + + case 'o': + /* LS3 - Locking Shift 3 */ + term->charsets.selected = G3; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); + break; + + case 'D': + term_linefeed(term); + break; + + case 'E': + term_carriage_return(term); + term_linefeed(term); + break; + + case 'H': + tab_set(term); + break; + + case 'M': + term_reverse_index(term); + break; + + case 'N': + /* SS2 - Single Shift 2 */ + term_single_shift(term, G2); + break; + + case 'O': + /* SS3 - Single Shift 3 */ + term_single_shift(term, G3); + break; + + case '\\': + /* ST - String Terminator */ + break; + + case '=': + term->keypad_keys_mode = KEYPAD_APPLICATION; + break; + + case '>': + term->keypad_keys_mode = KEYPAD_NUMERICAL; + break; + + default: + UNHANDLED(); + break; + } + break; /* private[0] == 0 */ + + // Designate character set + case '(': // G0 + case ')': // G1 + case '*': // G2 + case '+': // G3 + switch (final) { + case '0': { + size_t idx = term->vt.private - '('; + xassert(idx <= G3); + term->charsets.set[idx] = CHARSET_GRAPHIC; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); + break; + } + + case 'B': { + size_t idx = term->vt.private - '('; + xassert(idx <= G3); + term->charsets.set[idx] = CHARSET_ASCII; + term->bits_affecting_ascii_printer.charset = + term->charsets.set[term->charsets.selected] != CHARSET_ASCII; + term_update_ascii_printer(term); + break; + } + } + break; + + case '#': + switch (final) { + case '8': /* DECALN */ + sixel_overwrite_by_rectangle(term, 0, 0, term->rows, term->cols); + + term->scroll_region.start = 0; + term->scroll_region.end = term->rows; + + for (int r = 0; r < term->rows; r++) + term_fill(term, r, 0, 'E', term->cols, false); + + term_cursor_home(term); + break; + } + break; /* private[0] == '#' */ + + } +} + +static void +action_csi_dispatch(struct terminal *term, uint8_t c) +{ + csi_dispatch(term, c); +} + +static void +action_osc_start(struct terminal *term, uint8_t c) +{ + term->vt.osc.idx = 0; +} + +static void +action_osc_end(struct terminal *term, uint8_t c) +{ + struct vt *vt = &term->vt; + + if (!osc_ensure_size(term, vt->osc.idx + 1)) + return; + + vt->osc.data[vt->osc.idx] = '\0'; + vt->osc.bel = c == '\a'; + osc_dispatch(term); + + if (unlikely(vt->osc.idx >= 4096)) { + free(vt->osc.data); + vt->osc.data = NULL; + vt->osc.size = 0; + } +} + +static void +action_osc_put(struct terminal *term, uint8_t c) +{ + if (!osc_ensure_size(term, term->vt.osc.idx + 1)) + return; + term->vt.osc.data[term->vt.osc.idx++] = c; +} + +static void +action_hook(struct terminal *term, uint8_t c) +{ + dcs_hook(term, c); +} + +static void +action_unhook(struct terminal *term, uint8_t c) +{ + dcs_unhook(term); +} + +static void +action_put(struct terminal *term, uint8_t c) +{ + dcs_put(term, c); +} + +static void +action_utf8_print(struct terminal *term, char32_t wc) +{ + term_process_and_print_non_ascii(term, wc); +} + +static void +action_utf8_21(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 0x1f) << 6) | (utf8[1] & 0x3f) + term->vt.utf8 = (c & 0x1f) << 6; +} + +static void +action_utf8_22(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 0x1f) << 6) | (utf8[1] & 0x3f) + term->vt.utf8 |= c & 0x3f; + action_utf8_print(term, term->vt.utf8); +} + +static void +action_utf8_31(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 0xf) << 12) | ((utf8[1] & 0x3f) << 6) | (utf8[2] & 0x3f) + term->vt.utf8 = (c & 0x0f) << 12; +} + +static void +action_utf8_32(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 0xf) << 12) | ((utf8[1] & 0x3f) << 6) | (utf8[2] & 0x3f) + term->vt.utf8 |= (c & 0x3f) << 6; +} + +static void +action_utf8_33(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 0xf) << 12) | ((utf8[1] & 0x3f) << 6) | (utf8[2] & 0x3f) + term->vt.utf8 |= c & 0x3f; + + const char32_t utf32 = term->vt.utf8; + if (unlikely(utf32 >= 0xd800 && utf32 <= 0xdfff)) { + /* Invalid sequence - invalid UTF-16 surrogate halves */ + return; + } + + /* Note: the E0 range contains overlong encodings. We don't try to + detect, as they'll still decode to valid UTF-32. */ + + action_utf8_print(term, term->vt.utf8); +} + +static void +action_utf8_41(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f); + term->vt.utf8 = (c & 0x07) << 18; +} + +static void +action_utf8_42(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f); + term->vt.utf8 |= (c & 0x3f) << 12; +} + +static void +action_utf8_43(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f); + term->vt.utf8 |= (c & 0x3f) << 6; +} + +static void +action_utf8_44(struct terminal *term, uint8_t c) +{ + // wc = ((utf8[0] & 7) << 18) | ((utf8[1] & 0x3f) << 12) | ((utf8[2] & 0x3f) << 6) | (utf8[3] & 0x3f); + term->vt.utf8 |= c & 0x3f; + + const char32_t utf32 = term->vt.utf8; + + if (unlikely(utf32 > 0x10FFFF)) { + /* Invalid UTF-8 */ + return; + } + + /* Note: the F0 range contains overlong encodings. We don't try to + detect, as they'll still decode to valid UTF-32. */ + + action_utf8_print(term, term->vt.utf8); +} + +IGNORE_WARNING("-Wpedantic") + +static enum state +anywhere(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x18: action_execute(term, data); return STATE_GROUND; + case 0x1a: action_execute(term, data); return STATE_GROUND; + case 0x1b: action_clear(term); return STATE_ESCAPE; + + /* 8-bit C1 control characters (not supported) */ + case 0x80 ... 0x9f: return STATE_GROUND; + } + + return term->vt.state; +} + +static enum state +state_ground_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_execute(term, data); return STATE_GROUND; + + /* modified from 0x20..0x7f to 0x20..0x7e, since 0x7f is DEL, which is a zero-width character */ + case 0x20 ... 0x7e: action_print(term, data); return STATE_GROUND; + + case 0xc2 ... 0xdf: action_utf8_21(term, data); return STATE_UTF8_21; + case 0xe0 ... 0xef: action_utf8_31(term, data); return STATE_UTF8_31; + case 0xf0 ... 0xf4: action_utf8_41(term, data); return STATE_UTF8_41; + } + + return anywhere(term, data); +} + +static enum state +state_escape_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_execute(term, data); return STATE_ESCAPE; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_ESCAPE_INTERMEDIATE; + case 0x30 ... 0x4f: action_esc_dispatch(term, data); return STATE_GROUND; + case 0x50: action_clear(term); return STATE_DCS_ENTRY; + case 0x51 ... 0x57: action_esc_dispatch(term, data); return STATE_GROUND; + case 0x58: return STATE_SOS_PM_APC_STRING; + case 0x59: action_esc_dispatch(term, data); return STATE_GROUND; + case 0x5a: action_esc_dispatch(term, data); return STATE_GROUND; + case 0x5b: action_clear(term); return STATE_CSI_ENTRY; + case 0x5c: action_esc_dispatch(term, data); return STATE_GROUND; + case 0x5d: action_osc_start(term, data); return STATE_OSC_STRING; + case 0x5e ... 0x5f: return STATE_SOS_PM_APC_STRING; + case 0x60 ... 0x7e: action_esc_dispatch(term, data); return STATE_GROUND; + case 0x7f: action_ignore(term); return STATE_ESCAPE; + } + + return anywhere(term, data); +} + +static enum state +state_escape_intermediate_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_execute(term, data); return STATE_ESCAPE_INTERMEDIATE; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_ESCAPE_INTERMEDIATE; + case 0x30 ... 0x7e: action_esc_dispatch(term, data); return STATE_GROUND; + case 0x7f: action_ignore(term); return STATE_ESCAPE_INTERMEDIATE; + } + + return anywhere(term, data); +} + +static enum state +state_csi_entry_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_ENTRY; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE; + case 0x30 ... 0x39: action_param(term, data); return STATE_CSI_PARAM; + case 0x3a: action_param_new_subparam(term, data); return STATE_CSI_PARAM; + case 0x3b: action_param_new(term, data); return STATE_CSI_PARAM; + + case 0x3c ... 0x3f: action_collect(term, data); return STATE_CSI_PARAM; + case 0x40 ... 0x7e: action_csi_dispatch(term, data); return STATE_GROUND; + case 0x7f: action_ignore(term); return STATE_CSI_ENTRY; + } + + return anywhere(term, data); +} + +static enum state +state_csi_param_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_PARAM; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE; + + case 0x30 ... 0x39: action_param(term, data); return STATE_CSI_PARAM; + case 0x3a: action_param_new_subparam(term, data); return STATE_CSI_PARAM; + case 0x3b: action_param_new(term, data); return STATE_CSI_PARAM; + + case 0x3c ... 0x3f: return STATE_CSI_IGNORE; + case 0x40 ... 0x7e: action_csi_dispatch(term, data); return STATE_GROUND; + case 0x7f: action_ignore(term); return STATE_CSI_PARAM; + } + + return anywhere(term, data); +} + +static enum state +state_csi_intermediate_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_INTERMEDIATE; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_CSI_INTERMEDIATE; + case 0x30 ... 0x3f: return STATE_CSI_IGNORE; + case 0x40 ... 0x7e: action_csi_dispatch(term, data); return STATE_GROUND; + case 0x7f: action_ignore(term); return STATE_CSI_INTERMEDIATE; + } + + return anywhere(term, data); +} + +static enum state +state_csi_ignore_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_execute(term, data); return STATE_CSI_IGNORE; + + case 0x20 ... 0x3f: action_ignore(term); return STATE_CSI_IGNORE; + case 0x40 ... 0x7e: return STATE_GROUND; + case 0x7f: action_ignore(term); return STATE_CSI_IGNORE; + } + + return anywhere(term, data); +} + +static enum state +state_osc_string_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + + /* Note: original was 20-7f, but I changed to 20-ff to include utf-8. Don't forget to add EXECUTE to 8-bit C1 if we implement that. */ + default: action_osc_put(term, data); return STATE_OSC_STRING; + + case 0x07: action_osc_end(term, data); return STATE_GROUND; + + case 0x00 ... 0x06: + case 0x08 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_ignore(term); return STATE_OSC_STRING; + + + case 0x18: + case 0x1a: action_osc_end(term, data); action_execute(term, data); return STATE_GROUND; + + case 0x1b: action_osc_end(term, data); action_clear(term); return STATE_ESCAPE; + } +} + +static enum state +state_dcs_entry_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_ignore(term); return STATE_DCS_ENTRY; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_DCS_INTERMEDIATE; + case 0x30 ... 0x39: action_param(term, data); return STATE_DCS_PARAM; + case 0x3a: return STATE_DCS_IGNORE; + case 0x3b: action_param_new(term, data); return STATE_DCS_PARAM; + case 0x3c ... 0x3f: action_collect(term, data); return STATE_DCS_PARAM; + case 0x40 ... 0x7e: action_hook(term, data); return STATE_DCS_PASSTHROUGH; + case 0x7f: action_ignore(term); return STATE_DCS_ENTRY; + } + + return anywhere(term, data); +} + +static enum state +state_dcs_param_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_ignore(term); return STATE_DCS_PARAM; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_DCS_INTERMEDIATE; + case 0x30 ... 0x39: action_param(term, data); return STATE_DCS_PARAM; + case 0x3a: return STATE_DCS_IGNORE; + case 0x3b: action_param_new(term, data); return STATE_DCS_PARAM; + case 0x3c ... 0x3f: return STATE_DCS_IGNORE; + case 0x40 ... 0x7e: action_hook(term, data); return STATE_DCS_PASSTHROUGH; + case 0x7f: action_ignore(term); return STATE_DCS_PARAM; + } + + return anywhere(term, data); +} + +static enum state +state_dcs_intermediate_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: action_ignore(term); return STATE_DCS_INTERMEDIATE; + + case 0x20 ... 0x2f: action_collect(term, data); return STATE_DCS_INTERMEDIATE; + case 0x30 ... 0x3f: return STATE_DCS_IGNORE; + case 0x40 ... 0x7e: action_hook(term, data); return STATE_DCS_PASSTHROUGH; + case 0x7f: action_ignore(term); return STATE_DCS_INTERMEDIATE; + } + + return anywhere(term, data); +} + +static enum state +state_dcs_ignore_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x1f: + case 0x20 ... 0x7f: action_ignore(term); return STATE_DCS_IGNORE; + } + + return anywhere(term, data); +} + +static enum state +state_dcs_passthrough_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x7e: action_put(term, data); return STATE_DCS_PASSTHROUGH; + + case 0x7f: action_ignore(term); return STATE_DCS_PASSTHROUGH; + + /* Anywhere */ + case 0x18: action_unhook(term, data); action_execute(term, data); return STATE_GROUND; + case 0x1a: action_unhook(term, data); action_execute(term, data); return STATE_GROUND; + case 0x1b: action_unhook(term, data); action_clear(term); return STATE_ESCAPE; + + /* 8-bit C1 control characters (not supported) */ + case 0x80 ... 0x9f: action_unhook(term, data); return STATE_GROUND; + + default: return STATE_DCS_PASSTHROUGH; + } +} + +static enum state +state_sos_pm_apc_string_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x00 ... 0x17: + case 0x19: + case 0x1c ... 0x7f: action_ignore(term); return STATE_SOS_PM_APC_STRING; + } + + return anywhere(term, data); +} + +static enum state +state_utf8_21_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x80 ... 0xbf: action_utf8_22(term, data); return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); + } +} + +static enum state +state_utf8_31_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x80 ... 0xbf: action_utf8_32(term, data); return STATE_UTF8_32; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); + } +} + +static enum state +state_utf8_32_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x80 ... 0xbf: action_utf8_33(term, data); return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); + } +} + +static enum state +state_utf8_41_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x80 ... 0xbf: action_utf8_42(term, data); return STATE_UTF8_42; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); + } +} + +static enum state +state_utf8_42_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x80 ... 0xbf: action_utf8_43(term, data); return STATE_UTF8_43; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); + } +} + +static enum state +state_utf8_43_switch(struct terminal *term, uint8_t data) +{ + switch (data) { + /* exit current enter new state */ + case 0x80 ... 0xbf: action_utf8_44(term, data); return STATE_GROUND; + default: action_utf8_print(term, 0xfffd); return state_ground_switch(term, data); + } +} + +UNIGNORE_WARNINGS + +void +vt_from_slave(struct terminal *term, const uint8_t *data, size_t len) +{ + enum state current_state = term->vt.state; + + const uint8_t *p = data; + for (size_t i = 0; i < len; i++, p++) { + switch (current_state) { + case STATE_GROUND: current_state = state_ground_switch(term, *p); break; + case STATE_ESCAPE: current_state = state_escape_switch(term, *p); break; + case STATE_ESCAPE_INTERMEDIATE: current_state = state_escape_intermediate_switch(term, *p); break; + case STATE_CSI_ENTRY: current_state = state_csi_entry_switch(term, *p); break; + case STATE_CSI_PARAM: current_state = state_csi_param_switch(term, *p); break; + case STATE_CSI_INTERMEDIATE: current_state = state_csi_intermediate_switch(term, *p); break; + case STATE_CSI_IGNORE: current_state = state_csi_ignore_switch(term, *p); break; + case STATE_OSC_STRING: current_state = state_osc_string_switch(term, *p); break; + case STATE_DCS_ENTRY: current_state = state_dcs_entry_switch(term, *p); break; + case STATE_DCS_PARAM: current_state = state_dcs_param_switch(term, *p); break; + case STATE_DCS_INTERMEDIATE: current_state = state_dcs_intermediate_switch(term, *p); break; + case STATE_DCS_IGNORE: current_state = state_dcs_ignore_switch(term, *p); break; + case STATE_DCS_PASSTHROUGH: current_state = state_dcs_passthrough_switch(term, *p); break; + case STATE_SOS_PM_APC_STRING: current_state = state_sos_pm_apc_string_switch(term, *p); break; + + case STATE_UTF8_21: current_state = state_utf8_21_switch(term, *p); break; + case STATE_UTF8_31: current_state = state_utf8_31_switch(term, *p); break; + case STATE_UTF8_32: current_state = state_utf8_32_switch(term, *p); break; + case STATE_UTF8_41: current_state = state_utf8_41_switch(term, *p); break; + case STATE_UTF8_42: current_state = state_utf8_42_switch(term, *p); break; + case STATE_UTF8_43: current_state = state_utf8_43_switch(term, *p); break; + } + + term->vt.state = current_state; + } +} diff --git a/vt.h b/vt.h new file mode 100644 index 0000000..6695846 --- /dev/null +++ b/vt.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +#include "terminal.h" + +void vt_from_slave(struct terminal *term, const uint8_t *data, size_t len); + +static inline int +vt_param_get(const struct terminal *term, size_t idx, int default_value) +{ + /* + * We zero excess bits in parsed param values. In most cases this will + * effectively be a no-op; but it prevents negative returns for edge + * cases involving unusually large values. + */ + static_assert(INT_MAX >= 0x7fffffff, "POSIX requires INT_MAX >= 0x7fffffff"); + const unsigned value_mask = 0x7fffffff; + + if (term->vt.params.idx > idx) { + unsigned value = term->vt.params.v[idx].value & value_mask; + return value != 0 ? (int)value : default_value; + } + + return default_value; +} diff --git a/wayland.c b/wayland.c new file mode 100644 index 0000000..c2b6194 --- /dev/null +++ b/wayland.c @@ -0,0 +1,2858 @@ +#include "wayland.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#define LOG_MODULE "wayland" +#define LOG_ENABLE_DBG 0 +#include "log.h" + +#include "config.h" +#include "terminal.h" +#include "ime.h" +#include "input.h" +#include "render.h" +#include "selection.h" +#include "shm.h" +#include "shm-formats.h" +#include "util.h" +#include "xmalloc.h" + +static void +csd_reload_font(struct wl_window *win, float old_scale) +{ + struct terminal *term = win->term; + const struct config *conf = term->conf; + + const float scale = term->scale; + + bool enable_csd = win->csd_mode == CSD_YES && !win->is_fullscreen; + if (!enable_csd) + return; + if (win->csd.font != NULL && scale == old_scale) + return; + + fcft_destroy(win->csd.font); + + const char *patterns[conf->csd.font.count]; + for (size_t i = 0; i < conf->csd.font.count; i++) + patterns[i] = conf->csd.font.arr[i].pattern; + + char pixelsize[32]; + snprintf(pixelsize, sizeof(pixelsize), "pixelsize=%u", + (int)roundf(conf->csd.title_height * scale * 1 / 2)); + + LOG_DBG("loading CSD font \"%s:%s\" (old-scale=%.2f, scale=%.2f)", + patterns[0], pixelsize, old_scale, scale); + + win->csd.font = fcft_from_name(conf->csd.font.count, patterns, pixelsize); +} + +static void +csd_instantiate(struct wl_window *win) +{ + struct wayland *wayl = win->term->wl; + xassert(wayl != NULL); + + for (size_t i = 0; i < CSD_SURF_MINIMIZE; i++) { + bool ret = wayl_win_subsurface_new(win, &win->csd.surface[i], true); + xassert(ret); + } + + for (size_t i = CSD_SURF_MINIMIZE; i < CSD_SURF_COUNT; i++) { + bool ret = wayl_win_subsurface_new_with_custom_parent( + win, win->csd.surface[CSD_SURF_TITLE].surface.surf, &win->csd.surface[i], + true); + xassert(ret); + } + + csd_reload_font(win, -1.); +} + +static void +csd_destroy(struct wl_window *win) +{ + struct terminal *term = win->term; + + fcft_destroy(term->window->csd.font); + term->window->csd.font = NULL; + + for (size_t i = 0; i < ALEN(win->csd.surface); i++) + wayl_win_subsurface_destroy(&win->csd.surface[i]); + shm_purge(term->render.chains.csd); +} + +static void +seat_add_data_device(struct seat *seat) +{ + if (seat->wayl->data_device_manager == NULL) + return; + + if (seat->data_device != NULL) { + /* TODO: destroy old device + clipboard data? */ + return; + } + + struct wl_data_device *data_device = wl_data_device_manager_get_data_device( + seat->wayl->data_device_manager, seat->wl_seat); + + if (data_device == NULL) + return; + + seat->data_device = data_device; + wl_data_device_add_listener(data_device, &data_device_listener, seat); +} + +static void +seat_add_primary_selection(struct seat *seat) +{ + if (seat->wayl->primary_selection_device_manager == NULL) + return; + + if (seat->primary_selection_device != NULL) + return; + + struct zwp_primary_selection_device_v1 *primary_selection_device + = zwp_primary_selection_device_manager_v1_get_device( + seat->wayl->primary_selection_device_manager, seat->wl_seat); + + if (primary_selection_device == NULL) + return; + + seat->primary_selection_device = primary_selection_device; + zwp_primary_selection_device_v1_add_listener( + primary_selection_device, &primary_selection_device_listener, seat); +} + +static void +seat_add_text_input(struct seat *seat) +{ +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (seat->wayl->text_input_manager == NULL) + return; + + struct zwp_text_input_v3 *text_input + = zwp_text_input_manager_v3_get_text_input( + seat->wayl->text_input_manager, seat->wl_seat); + + if (text_input == NULL) + return; + + seat->wl_text_input = text_input; + zwp_text_input_v3_add_listener(text_input, &text_input_listener, seat); +#endif +} + +static void +seat_add_key_bindings(struct seat *seat) +{ + key_binding_new_for_seat(seat->wayl->key_binding_manager, seat); +} + +static void +seat_destroy(struct seat *seat) +{ + if (seat == NULL) + return; + + tll_free(seat->mouse.buttons); + key_binding_remove_seat(seat->wayl->key_binding_manager, seat); + + if (seat->kbd.xkb_compose_state != NULL) + xkb_compose_state_unref(seat->kbd.xkb_compose_state); + if (seat->kbd.xkb_compose_table != NULL) + xkb_compose_table_unref(seat->kbd.xkb_compose_table); + if (seat->kbd.xkb_keymap != NULL) + xkb_keymap_unref(seat->kbd.xkb_keymap); + if (seat->kbd.xkb_state != NULL) + xkb_state_unref(seat->kbd.xkb_state); + if (seat->kbd.xkb != NULL) + xkb_context_unref(seat->kbd.xkb); + + if (seat->kbd.repeat.fd >= 0) + fdm_del(seat->wayl->fdm, seat->kbd.repeat.fd); + + if (seat->pointer.theme != NULL) + wl_cursor_theme_destroy(seat->pointer.theme); + if (seat->pointer.surface.surf != NULL) + wl_surface_destroy(seat->pointer.surface.surf); + if (seat->pointer.surface.viewport != NULL) + wp_viewport_destroy(seat->pointer.surface.viewport); + if (seat->pointer.xcursor_callback != NULL) + wl_callback_destroy(seat->pointer.xcursor_callback); + + if (seat->clipboard.data_source != NULL) + wl_data_source_destroy(seat->clipboard.data_source); + if (seat->clipboard.data_offer != NULL) + wl_data_offer_destroy(seat->clipboard.data_offer); + if (seat->primary.data_source != NULL) + zwp_primary_selection_source_v1_destroy(seat->primary.data_source); + if (seat->primary.data_offer != NULL) + zwp_primary_selection_offer_v1_destroy(seat->primary.data_offer); + if (seat->primary_selection_device != NULL) + zwp_primary_selection_device_v1_destroy(seat->primary_selection_device); + if (seat->data_device != NULL) + wl_data_device_release(seat->data_device); + if (seat->pointer.shape_device != NULL) + wp_cursor_shape_device_v1_destroy(seat->pointer.shape_device); + if (seat->wl_keyboard != NULL) + wl_keyboard_release(seat->wl_keyboard); + if (seat->wl_pointer != NULL) + wl_pointer_release(seat->wl_pointer); + if (seat->wl_touch != NULL) + wl_touch_release(seat->wl_touch); + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (seat->wl_text_input != NULL) + zwp_text_input_v3_destroy(seat->wl_text_input); +#endif + + if (seat->wl_seat != NULL) + wl_seat_release(seat->wl_seat); + + ime_reset_pending(seat); + free(seat->clipboard.text); + free(seat->primary.text); + free(seat->pointer.last_custom_xcursor); + free(seat->name); +} + +static void +shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) +{ + struct wayland *wayl = data; + + switch (format) { + case WL_SHM_FORMAT_ARGB2101010: wayl->shm_have_argb2101010 = true; break; + case WL_SHM_FORMAT_ABGR2101010: wayl->shm_have_abgr2101010 = true; break; + case WL_SHM_FORMAT_ABGR16161616: wayl->shm_have_abgr161616 = true; break; + } + +#if defined(_DEBUG) && LOG_ENABLE_DBG == 1 + bool have_description = false; + const char c4 = (format >> 24) & 0xff; + const char c3 = (format >> 16) & 0xff; + const char c2 = (format >> 8) & 0xff; + const char c1 = (format >> 0) & 0xff; + + for (size_t i = 0; i < ALEN(shm_formats); i++) { + if (shm_formats[i].format == format) { + LOG_DBG("shm: 0x%08x: %c%c%c%c - %s", + format, + isprint(c1) ? c1 : ' ', + isprint(c2) ? c2 : ' ', + isprint(c3) ? c3 : ' ', + isprint(c4) ? c4 : ' ', + shm_formats[i].description); + have_description = true; + break; + } + } + + if (!have_description) + LOG_DBG("shm: 0x%08x: %c%c%c%c - unknown", + format, + isprint(c1) ? c1 : ' ', + isprint(c2) ? c2 : ' ', + isprint(c3) ? c3 : ' ', + isprint(c4) ? c4 : ' '); +#endif +} + +static const struct wl_shm_listener shm_listener = { + .format = &shm_format, +}; + +static void +xdg_wm_base_ping(void *data, struct xdg_wm_base *shell, uint32_t serial) +{ + LOG_DBG("wm base ping"); + xdg_wm_base_pong(shell, serial); +} + +static const struct xdg_wm_base_listener xdg_wm_base_listener = { + .ping = &xdg_wm_base_ping, +}; + +static void +seat_handle_capabilities(void *data, struct wl_seat *wl_seat, + enum wl_seat_capability caps) +{ + struct seat *seat = data; + xassert(seat->wl_seat == wl_seat); + + LOG_DBG("%s: keyboard=%s, pointer=%s, touch=%s", seat->name, + (caps & WL_SEAT_CAPABILITY_KEYBOARD) ? "yes" : "no", + (caps & WL_SEAT_CAPABILITY_POINTER) ? "yes" : "no", + (caps & WL_SEAT_CAPABILITY_TOUCH) ? "yes" : "no"); + + if (caps & WL_SEAT_CAPABILITY_KEYBOARD) { + if (seat->wl_keyboard == NULL) { + seat->wl_keyboard = wl_seat_get_keyboard(wl_seat); + wl_keyboard_add_listener(seat->wl_keyboard, &keyboard_listener, seat); + } + } else { + if (seat->wl_keyboard != NULL) { + wl_keyboard_release(seat->wl_keyboard); + seat->wl_keyboard = NULL; + } + } + + if (caps & WL_SEAT_CAPABILITY_POINTER) { + if (seat->wl_pointer == NULL) { + xassert(seat->pointer.surface.surf == NULL); + seat->pointer.surface.surf = + wl_compositor_create_surface(seat->wayl->compositor); + + if (seat->pointer.surface.surf == NULL) { + LOG_ERR("%s: failed to create pointer surface", seat->name); + return; + } + + if (seat->wayl->viewporter != NULL) { + xassert(seat->pointer.surface.viewport == NULL); + seat->pointer.surface.viewport = wp_viewporter_get_viewport( + seat->wayl->viewporter, seat->pointer.surface.surf); + + if (seat->pointer.surface.viewport == NULL) { + LOG_ERR("%s: failed to create pointer viewport", seat->name); + wl_surface_destroy(seat->pointer.surface.surf); + seat->pointer.surface.surf = NULL; + return; + } + } + + seat->wl_pointer = wl_seat_get_pointer(wl_seat); + wl_pointer_add_listener(seat->wl_pointer, &pointer_listener, seat); + + if (seat->wayl->cursor_shape_manager != NULL) { + xassert(seat->pointer.shape_device == NULL); + seat->pointer.shape_device = wp_cursor_shape_manager_v1_get_pointer( + seat->wayl->cursor_shape_manager, seat->wl_pointer); + } + } + } else { + if (seat->wl_pointer != NULL) { + if (seat->pointer.shape_device != NULL) { + wp_cursor_shape_device_v1_destroy(seat->pointer.shape_device); + seat->pointer.shape_device = NULL; + } + + wl_pointer_release(seat->wl_pointer); + wl_surface_destroy(seat->pointer.surface.surf); + + if (seat->pointer.surface.viewport != NULL) { + wp_viewport_destroy(seat->pointer.surface.viewport); + seat->pointer.surface.viewport = NULL; + } + + if (seat->pointer.theme != NULL) + wl_cursor_theme_destroy(seat->pointer.theme); + + if (seat->wl_touch != NULL && + seat->touch.state == TOUCH_STATE_INHIBITED) + { + seat->touch.state = TOUCH_STATE_IDLE; + } + + seat->wl_pointer = NULL; + seat->pointer.surface.surf = NULL; + seat->pointer.theme = NULL; + seat->pointer.cursor = NULL; + } + } + + if (caps & WL_SEAT_CAPABILITY_TOUCH) { + if (seat->wl_touch == NULL) { + seat->wl_touch = wl_seat_get_touch(wl_seat); + wl_touch_add_listener(seat->wl_touch, &touch_listener, seat); + + seat->touch.state = TOUCH_STATE_IDLE; + } + } else { + if (seat->wl_touch != NULL) { + wl_touch_release(seat->wl_touch); + seat->wl_touch = NULL; + } + + seat->touch.state = TOUCH_STATE_INHIBITED; + } +} + +static void +seat_handle_name(void *data, struct wl_seat *wl_seat, const char *name) +{ + struct seat *seat = data; + free(seat->name); + seat->name = xstrdup(name); +} + +static const struct wl_seat_listener seat_listener = { + .capabilities = seat_handle_capabilities, + .name = seat_handle_name, +}; + +static void +update_term_for_output_change(struct terminal *term) +{ + const float old_scale = term->scale; + const float logical_width = term->width / old_scale; + const float logical_height = term->height / old_scale; + + /* Note: order matters! term_update_scale() must come first */ + bool scale_updated = term_update_scale(term); + bool fonts_updated = term_font_dpi_changed(term, old_scale); + term_font_subpixel_changed(term); + + csd_reload_font(term->window, old_scale); + + enum resize_options resize_opts = RESIZE_KEEP_GRID; + + if (fonts_updated) { + /* + * If the fonts have been updated, the cell dimensions have + * changed. This requires a "forced" resize, since the surface + * buffer dimensions may not have been updated (in which case + * render_resize() normally shortcuts and returns early). + */ + resize_opts |= RESIZE_FORCE; + } else if (!scale_updated) { + /* No need to resize if neither scale nor fonts have changed */ + return; + } else if (term->conf->dpi_aware) { + /* + * If fonts are sized according to DPI, it is possible for the cell + * size to remain the same when display scale changes. This will not + * change the surface buffer dimensions, but will change the logical + * size of the window. To ensure that the compositor is made aware of + * the proper logical size, force a resize rather than allowing + * render_resize() to shortcut the notification if the buffer + * dimensions remain the same. + */ + resize_opts |= RESIZE_FORCE; + } + + render_resize( + term, + (int)roundf(logical_width), + (int)roundf(logical_height), + resize_opts); +} + +static void +update_terms_on_monitor(struct monitor *mon) +{ + struct wayland *wayl = mon->wayl; + + tll_foreach(wayl->terms, it) { + struct terminal *term = it->item; + + tll_foreach(term->window->on_outputs, it2) { + if (it2->item == mon) { + update_term_for_output_change(term); + break; + } + } + } +} + +static void +output_update_ppi(struct monitor *mon) +{ + if (mon->dim.mm.width <= 0 || mon->dim.mm.height <= 0) + return; + + double x_inches = mon->dim.mm.width * 0.03937008; + double y_inches = mon->dim.mm.height * 0.03937008; + + const int width = mon->dim.px_real.width; + const int height = mon->dim.px_real.height; + + mon->ppi.real.x = mon->dim.px_real.width / x_inches; + mon->ppi.real.y = mon->dim.px_real.height / y_inches; + + /* The *logical* size is affected by the transform */ + switch (mon->transform) { + case WL_OUTPUT_TRANSFORM_90: + case WL_OUTPUT_TRANSFORM_270: + case WL_OUTPUT_TRANSFORM_FLIPPED_90: + case WL_OUTPUT_TRANSFORM_FLIPPED_270: { + int swap = x_inches; + x_inches = y_inches; + y_inches = swap; + break; + } + + case WL_OUTPUT_TRANSFORM_NORMAL: + case WL_OUTPUT_TRANSFORM_180: + case WL_OUTPUT_TRANSFORM_FLIPPED: + case WL_OUTPUT_TRANSFORM_FLIPPED_180: + break; + } + + const int scaled_width = mon->dim.px_scaled.width; + const int scaled_height = mon->dim.px_scaled.height; + + mon->ppi.scaled.x = scaled_width / x_inches; + mon->ppi.scaled.y = scaled_height / y_inches; + + const double px_diag_physical = sqrt(pow(width, 2) + pow(height, 2)); + mon->dpi.physical = width == 0 && height == 0 + ? 96. + : px_diag_physical / mon->inch; + + const double px_diag_scaled = sqrt(pow(scaled_width, 2) + pow(scaled_height, 2)); + mon->dpi.scaled = scaled_width == 0 && scaled_height == 0 + ? 96. + : px_diag_scaled / mon->inch * mon->scale; + + if (mon->dpi.physical > 1000) { + if (mon->name != NULL) { + LOG_WARN("%s: DPI=%f (physical) is unreasonable, using 96 instead", + mon->name, mon->dpi.physical); + } + mon->dpi.physical = 96; + } + + if (mon->dpi.scaled > 1000) { + if (mon->name != NULL) { + LOG_WARN("%s: DPI=%f (logical) is unreasonable, using 96 instead", + mon->name, mon->dpi.scaled); + } + mon->dpi.scaled = 96; + } +} + +static void +output_geometry(void *data, struct wl_output *wl_output, int32_t x, int32_t y, + int32_t physical_width, int32_t physical_height, + int32_t subpixel, const char *make, const char *model, + int32_t transform) +{ + struct monitor *mon = data; + + free(mon->make); + free(mon->model); + + mon->dim.mm.width = physical_width; + mon->dim.mm.height = physical_height; + mon->inch = sqrt(pow(mon->dim.mm.width, 2) + pow(mon->dim.mm.height, 2)) * 0.03937008; + mon->make = make != NULL ? xstrdup(make) : NULL; + mon->model = model != NULL ? xstrdup(model) : NULL; + mon->subpixel = subpixel; + mon->transform = transform; + + output_update_ppi(mon); +} + +static void +output_mode(void *data, struct wl_output *wl_output, uint32_t flags, + int32_t width, int32_t height, int32_t refresh) +{ + if ((flags & WL_OUTPUT_MODE_CURRENT) == 0) + return; + + struct monitor *mon = data; + mon->refresh = (float)refresh / 1000; + mon->dim.px_real.width = width; + mon->dim.px_real.height = height; + output_update_ppi(mon); +} + +static void +output_done(void *data, struct wl_output *wl_output) +{ + struct monitor *mon = data; + update_terms_on_monitor(mon); +} + +static void +output_scale(void *data, struct wl_output *wl_output, int32_t factor) +{ + struct monitor *mon = data; + mon->scale = factor; + output_update_ppi(mon); +} + +#if defined(WL_OUTPUT_NAME_SINCE_VERSION) +static void +output_name(void *data, struct wl_output *wl_output, const char *name) +{ + struct monitor *mon = data; + free(mon->name); + mon->name = name != NULL ? xstrdup(name) : NULL; +} +#endif + +#if defined(WL_OUTPUT_DESCRIPTION_SINCE_VERSION) +static void +output_description(void *data, struct wl_output *wl_output, + const char *description) +{ + struct monitor *mon = data; + free(mon->description); + mon->description = description != NULL ? xstrdup(description) : NULL; +} +#endif + +static const struct wl_output_listener output_listener = { + .geometry = &output_geometry, + .mode = &output_mode, + .done = &output_done, + .scale = &output_scale, +#if defined(WL_OUTPUT_NAME_SINCE_VERSION) + .name = &output_name, +#endif +#if defined(WL_OUTPUT_DESCRIPTION_SINCE_VERSION) + .description = &output_description, +#endif +}; + +static void +xdg_output_handle_logical_position( + void *data, struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y) +{ + struct monitor *mon = data; + mon->x = x; + mon->y = y; +} + +static void +xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, + int32_t width, int32_t height) +{ + struct monitor *mon = data; + mon->dim.px_scaled.width = width; + mon->dim.px_scaled.height = height; + output_update_ppi(mon); +} + +static void +xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) +{ + struct monitor *mon = data; + update_terms_on_monitor(mon); +} + +static void +xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, + const char *name) +{ + struct monitor *mon = data; + free(mon->name); + mon->name = name != NULL ? xstrdup(name) : NULL; +} + +static void +xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, + const char *description) +{ + struct monitor *mon = data; + free(mon->description); + mon->description = description != NULL ? xstrdup(description) : NULL; +} + +static const struct zxdg_output_v1_listener xdg_output_listener = { + .logical_position = xdg_output_handle_logical_position, + .logical_size = xdg_output_handle_logical_size, + .done = xdg_output_handle_done, + .name = xdg_output_handle_name, + .description = xdg_output_handle_description, +}; + +static void +clock_id(void *data, struct wp_presentation *wp_presentation, uint32_t clk_id) +{ + struct wayland *wayl = data; + wayl->presentation_clock_id = clk_id; + LOG_DBG("presentation clock ID: %u", clk_id); +} + +static const struct wp_presentation_listener presentation_listener = { + .clock_id = &clock_id, +}; + +static void +color_manager_create_image_description(struct wayland *wayl) +{ + if (!wayl->color_management.have_feat_parametric) + return; + + if (!wayl->color_management.have_primaries_srgb) + return; + + if (!wayl->color_management.have_tf_ext_linear) + return; + + struct wp_image_description_creator_params_v1 *params = + wp_color_manager_v1_create_parametric_creator(wayl->color_management.manager); + + wp_image_description_creator_params_v1_set_tf_named( + params, WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR); + + wp_image_description_creator_params_v1_set_primaries_named( + params, WP_COLOR_MANAGER_V1_PRIMARIES_SRGB); + + wayl->color_management.img_description = + wp_image_description_creator_params_v1_create(params); +} + +static void +color_manager_supported_intent(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t render_intent) +{ + struct wayland *wayl = data; + if (render_intent == WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL) + wayl->color_management.have_intent_perceptual = true; +} + +static void +color_manager_supported_feature(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t feature) +{ + struct wayland *wayl = data; + + if (feature == WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC) + wayl->color_management.have_feat_parametric = true; +} + +static void +color_manager_supported_tf_named(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t tf) +{ + struct wayland *wayl = data; + if (tf == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR) + wayl->color_management.have_tf_ext_linear = true; +} + +static void +color_manager_supported_primaries_named(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1, + uint32_t primaries) +{ + struct wayland *wayl = data; + if (primaries == WP_COLOR_MANAGER_V1_PRIMARIES_SRGB) + wayl->color_management.have_primaries_srgb = true; +} + +static void +color_manager_done(void *data, + struct wp_color_manager_v1 *wp_color_manager_v1) +{ + struct wayland *wayl = data; + color_manager_create_image_description(wayl); +} + +static const struct wp_color_manager_v1_listener color_manager_listener = { + .supported_intent = &color_manager_supported_intent, + .supported_feature = &color_manager_supported_feature, + .supported_primaries_named = &color_manager_supported_primaries_named, + .supported_tf_named = &color_manager_supported_tf_named, + .done = &color_manager_done, +}; + +static bool +verify_iface_version(const char *iface, uint32_t version, uint32_t wanted) +{ + if (version >= wanted) + return true; + + LOG_ERR("%s: need interface version %u, but compositor only implements %u", + iface, wanted, version); + return false; +} + +static void +surface_enter(void *data, struct wl_surface *wl_surface, + struct wl_output *wl_output) +{ + struct wl_window *win = data; + struct terminal *term = win->term; + + tll_foreach(term->wl->monitors, it) { + if (it->item.output == wl_output) { + LOG_DBG("mapped on %s", it->item.name); + tll_push_back(term->window->on_outputs, &it->item); + update_term_for_output_change(term); + return; + } + } + + LOG_ERR("mapped on unknown output"); +} + +static void +surface_leave(void *data, struct wl_surface *wl_surface, + struct wl_output *wl_output) +{ + struct wl_window *win = data; + struct terminal *term = win->term; + + tll_foreach(term->window->on_outputs, it) { + if (it->item->output != wl_output) + continue; + + LOG_DBG("unmapped from %s", it->item->name); + tll_remove(term->window->on_outputs, it); + update_term_for_output_change(term); + return; + } + + LOG_WARN("unmapped from unknown output"); +} + +#if defined(WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) +static void +surface_preferred_buffer_scale(void *data, struct wl_surface *surface, + int32_t scale) +{ + struct wl_window *win = data; + + if (win->preferred_buffer_scale == scale) + return; + + LOG_DBG("wl_surface preferred scale: %d -> %d", win->preferred_buffer_scale, scale); + + win->preferred_buffer_scale = scale; + update_term_for_output_change(win->term); +} + +static void +surface_preferred_buffer_transform(void *data, struct wl_surface *surface, + uint32_t transform) +{ + +} +#endif + +static const struct wl_surface_listener surface_listener = { + .enter = &surface_enter, + .leave = &surface_leave, +#if defined(WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) + .preferred_buffer_scale = &surface_preferred_buffer_scale, + .preferred_buffer_transform = &surface_preferred_buffer_transform, +#endif +}; + +static void +xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, + int32_t width, int32_t height, struct wl_array *states) +{ + bool is_activated = false; + bool is_fullscreen = false; + bool is_maximized = false; + bool is_resizing = false; + bool is_tiled_top = false; + bool is_tiled_bottom = false; + bool is_tiled_left = false; + bool is_tiled_right = false; + bool is_constrained_top = false; + bool is_constrained_bottom = false; + bool is_constrained_left = false; + bool is_constrained_right = false; + bool is_suspended UNUSED = false; + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + char state_str[2048]; + int state_chars = 0; + + static const char *const strings[] = { + [XDG_TOPLEVEL_STATE_MAXIMIZED] = "maximized", + [XDG_TOPLEVEL_STATE_FULLSCREEN] = "fullscreen", + [XDG_TOPLEVEL_STATE_RESIZING] = "resizing", + [XDG_TOPLEVEL_STATE_ACTIVATED] = "activated", + [XDG_TOPLEVEL_STATE_TILED_LEFT] = "tiled:left", + [XDG_TOPLEVEL_STATE_TILED_RIGHT] = "tiled:right", + [XDG_TOPLEVEL_STATE_TILED_TOP] = "tiled:top", + [XDG_TOPLEVEL_STATE_TILED_BOTTOM] = "tiled:bottom", +#if defined(XDG_TOPLEVEL_STATE_SUSPENDED_SINCE_VERSION) /* wayland-protocols >= 1.32 */ + [XDG_TOPLEVEL_STATE_SUSPENDED] = "suspended", +#endif +#if defined(XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION) + [XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT] = "constrained:left", + [XDG_TOPLEVEL_STATE_CONSTRAINED_RIGHT] = "constrained:right", + [XDG_TOPLEVEL_STATE_CONSTRAINED_TOP] = "constrained:top", + [XDG_TOPLEVEL_STATE_CONSTRAINED_BOTTOM] = "constrained:bottom", +#endif + }; +#endif + + enum xdg_toplevel_state *state; + wl_array_for_each(state, states) { + switch (*state) { + case XDG_TOPLEVEL_STATE_MAXIMIZED: is_maximized = true; break; + case XDG_TOPLEVEL_STATE_FULLSCREEN: is_fullscreen = true; break; + case XDG_TOPLEVEL_STATE_RESIZING: is_resizing = true; break; + case XDG_TOPLEVEL_STATE_ACTIVATED: is_activated = true; break; + case XDG_TOPLEVEL_STATE_TILED_LEFT: is_tiled_left = true; break; + case XDG_TOPLEVEL_STATE_TILED_RIGHT: is_tiled_right = true; break; + case XDG_TOPLEVEL_STATE_TILED_TOP: is_tiled_top = true; break; + case XDG_TOPLEVEL_STATE_TILED_BOTTOM: is_tiled_bottom = true; break; + +#if defined(XDG_TOPLEVEL_STATE_SUSPENDED_SINCE_VERSION) + case XDG_TOPLEVEL_STATE_SUSPENDED: is_suspended = true; break; +#endif +#if defined(XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION) + case XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT: is_constrained_left = true; break; + case XDG_TOPLEVEL_STATE_CONSTRAINED_RIGHT: is_constrained_right = true; break; + case XDG_TOPLEVEL_STATE_CONSTRAINED_TOP: is_constrained_top = true; break; + case XDG_TOPLEVEL_STATE_CONSTRAINED_BOTTOM: is_constrained_bottom = true; break; +#endif + } + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + if (*state >= 0 && *state < ALEN(strings)) { + state_chars += snprintf( + &state_str[state_chars], sizeof(state_str) - state_chars, + "%s, ", + strings[*state] != NULL ? strings[*state] : ""); + } +#endif + } + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + if (state_chars > 2) + state_str[state_chars - 2] = '\0'; + else + state_str[0] = '\0'; + + LOG_DBG("xdg-toplevel: configure: size=%dx%d, states=[%s]", + width, height, state_str); +#endif + + /* + * Changes done here are ignored until the configure event has + * been ack:ed in xdg_surface_configure(). + * + * So, just store the config data and apply it later, in + * xdg_surface_configure() after we've ack:ed the event. + */ + struct wl_window *win = data; + + win->configure.is_activated = is_activated; + win->configure.is_fullscreen = is_fullscreen; + win->configure.is_maximized = is_maximized; + win->configure.is_resizing = is_resizing; + win->configure.is_tiled_top = is_tiled_top; + win->configure.is_tiled_bottom = is_tiled_bottom; + win->configure.is_tiled_left = is_tiled_left; + win->configure.is_tiled_right = is_tiled_right; + win->configure.is_constrained_top = is_constrained_top; + win->configure.is_constrained_bottom = is_constrained_bottom; + win->configure.is_constrained_left = is_constrained_left; + win->configure.is_constrained_right = is_constrained_right; + win->configure.width = width; + win->configure.height = height; +} + +static void +xdg_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel) +{ + struct wl_window *win = data; + struct terminal *term = win->term; + LOG_DBG("xdg-toplevel: close"); + term_shutdown(term); +} + +#if defined(XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION) +static void +xdg_toplevel_configure_bounds(void *data, + struct xdg_toplevel *xdg_toplevel, + int32_t width, int32_t height) +{ + /* TODO: ensure we don't pick a bigger size */ +} +#endif + +#if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) +static void +xdg_toplevel_wm_capabilities(void *data, + struct xdg_toplevel *xdg_toplevel, + struct wl_array *caps) +{ + struct wl_window *win = data; + + win->wm_capabilities.maximize = false; + win->wm_capabilities.minimize = false; + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + char cap_str[2048]; + int cap_chars = 0; + + static const char *const strings[] = { + [XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU] = "window-menu", + [XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE] = "maximize", + [XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN] = "fullscreen", + [XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE] = "minimize", + }; +#endif + + enum xdg_toplevel_wm_capabilities *cap; + wl_array_for_each(cap, caps) { + switch (*cap) { + case XDG_TOPLEVEL_WM_CAPABILITIES_MAXIMIZE: + win->wm_capabilities.maximize = true; + break; + + case XDG_TOPLEVEL_WM_CAPABILITIES_MINIMIZE: + win->wm_capabilities.minimize = true; + break; + + case XDG_TOPLEVEL_WM_CAPABILITIES_WINDOW_MENU: + case XDG_TOPLEVEL_WM_CAPABILITIES_FULLSCREEN: + break; + } + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + if (*cap >= 0 && *cap < ALEN(strings)) { + cap_chars += snprintf( + &cap_str[cap_chars], sizeof(cap_str) - cap_chars, + "%s, ", + strings[*cap] != NULL ? strings[*cap] : ""); + } +#endif + } + +#if defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG + if (cap_chars > 2) + cap_str[cap_chars - 2] = '\0'; + else + cap_str[0] = '\0'; + + LOG_DBG("xdg-toplevel: wm-capabilities=[%s]", cap_str); +#endif +} +#endif + +static const struct xdg_toplevel_listener xdg_toplevel_listener = { + .configure = &xdg_toplevel_configure, + /*.close = */&xdg_toplevel_close, /* epoll-shim defines a macro 'close'... */ +#if defined(XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION) + .configure_bounds = &xdg_toplevel_configure_bounds, +#endif +#if defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) + .wm_capabilities = xdg_toplevel_wm_capabilities, +#endif +}; + +static void +xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, + uint32_t serial) +{ + LOG_DBG("xdg-surface: configure"); + + struct wl_window *win = data; + struct terminal *term = win->term; + + if (win->unmapped) { + /* + * https://codeberg.org/dnkl/foot/issues/1249 + * https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/3487 + * https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/3719 + * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/108 + */ + return; + } + + bool wasnt_configured = !win->is_configured; + bool was_resizing = win->is_resizing; + bool was_fullscreen = win->is_fullscreen; + bool csd_was_enabled = win->csd_mode == CSD_YES && !win->is_fullscreen; + int new_width = win->configure.width; + int new_height = win->configure.height; + + win->is_configured = true; + win->is_maximized = win->configure.is_maximized; + win->is_fullscreen = win->configure.is_fullscreen; + win->is_resizing = win->configure.is_resizing; + + win->is_tiled_top = win->configure.is_tiled_top; + win->is_tiled_bottom = win->configure.is_tiled_bottom; + win->is_tiled_left = win->configure.is_tiled_left; + win->is_tiled_right = win->configure.is_tiled_right; + + win->is_constrained_top = win->configure.is_constrained_top; + win->is_constrained_bottom = win->configure.is_constrained_bottom; + win->is_constrained_left = win->configure.is_constrained_left; + win->is_constrained_right = win->configure.is_constrained_right; + + win->is_tiled = (win->is_tiled_top || + win->is_tiled_bottom || + win->is_tiled_left || + win->is_tiled_right); + + win->csd_mode = win->configure.csd_mode; + + bool enable_csd = win->csd_mode == CSD_YES && !win->is_fullscreen; + if (!csd_was_enabled && enable_csd) + csd_instantiate(win); + else if (csd_was_enabled && !enable_csd) + csd_destroy(win); + + if (enable_csd && new_width > 0 && new_height > 0) { + if (wayl_win_csd_titlebar_visible(win)) + new_height -= win->term->conf->csd.title_height; + + if (wayl_win_csd_borders_visible(win)) { + new_height -= 2 * win->term->conf->csd.border_width_visible; + new_width -= 2 * win->term->conf->csd.border_width_visible; + } + } + + xdg_surface_ack_configure(xdg_surface, serial); + + enum resize_options opts = RESIZE_BY_CELLS; + +#if 1 + /* + * TODO: decide if we should do the last "forced" call when ending + * an interactive resize. + * + * Without it, the last TIOCSWINSZ sent to the client will be a + * scheduled one. I.e. there will be a small delay after the user + * has *stopped* resizing, and the client application receives the + * final size. + * + * Note: if we also disable content centering while resizing, then + * the last, forced, resize *is* necessary. + */ + if (was_resizing && !win->is_resizing) + opts |= RESIZE_FORCE; +#endif + + bool resized = render_resize(term, new_width, new_height, opts); + + if (win->configure.is_activated) + term_visual_focus_in(term); + else + term_visual_focus_out(term); + + /* + * Update opaque region if fullscreen state changed, also need to + * render, since we use different buffer types with and without + * alpha + */ + if (was_fullscreen != win->is_fullscreen) { + wayl_win_alpha_changed(win); + render_refresh(term); + } + + const bool will_render_soon = resized || + term->render.refresh.grid || + term->render.pending.grid; + + if (!will_render_soon) { + /* + * If we didn't resize, and aren't refreshing for other + * reasons, we won't be committing a new surface anytime + * soon. Some compositors require a commit in combination with + * an ack - make them happy. + */ + wl_surface_commit(win->surface.surf); + } + + if (wasnt_configured) + term_window_configured(term); +} + +static const struct xdg_surface_listener xdg_surface_listener = { + .configure = &xdg_surface_configure, +}; + +static void +xdg_toplevel_decoration_configure(void *data, + struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, + uint32_t mode) +{ + struct wl_window *win = data; + + xassert(win->term->conf->csd.preferred != CONF_CSD_PREFER_NONE); + switch (mode) { + case ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE: + LOG_INFO("using CSD decorations"); + win->configure.csd_mode = CSD_YES; + break; + + case ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE: + LOG_INFO("using SSD decorations"); + win->configure.csd_mode = CSD_NO; + break; + + default: + LOG_ERR("unimplemented: unknown XDG toplevel decoration mode: %u", mode); + break; + } +} + +static const struct zxdg_toplevel_decoration_v1_listener xdg_toplevel_decoration_listener = { + .configure = &xdg_toplevel_decoration_configure, +}; + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) +static void +ext_background_capabilities( + void *data, + struct ext_background_effect_manager_v1 *ext_background_effect_manager_v1, + uint32_t flags) +{ + struct wayland *wayl = data; + + wayl->have_background_blur = + !!(flags & EXT_BACKGROUND_EFFECT_MANAGER_V1_CAPABILITY_BLUR); + + LOG_DBG("compositor supports background blur: %s", + wayl->have_background_blur ? "yes" : "no"); +} + +static const struct ext_background_effect_manager_v1_listener background_manager_listener = { + .capabilities = &ext_background_capabilities, +}; +#endif /* HAVE_EXT_BACKGROUND_EFFECT */ + +static bool +fdm_repeat(struct fdm *fdm, int fd, int events, void *data) +{ + if (events & EPOLLHUP) + return false; + + struct seat *seat = data; + uint64_t expiration_count; + ssize_t ret = read( + seat->kbd.repeat.fd, &expiration_count, sizeof(expiration_count)); + + if (ret < 0) { + if (errno == EAGAIN) + return true; + + LOG_ERRNO("failed to read repeat key from repeat timer fd"); + return false; + } + + seat->kbd.repeat.dont_re_repeat = true; + for (size_t i = 0; i < expiration_count; i++) + input_repeat(seat, seat->kbd.repeat.key); + seat->kbd.repeat.dont_re_repeat = false; + return true; +} + +static void +handle_global(void *data, struct wl_registry *registry, + uint32_t name, const char *interface, uint32_t version) +{ + LOG_DBG("global: 0x%08x, interface=%s, version=%u", name, interface, version); + struct wayland *wayl = data; + + if (streq(interface, wl_compositor_interface.name)) { + const uint32_t required = 4; + if (!verify_iface_version(interface, version, required)) + return; + +#if defined (WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) + const uint32_t preferred = WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif + wayl->compositor = wl_registry_bind( + wayl->registry, name, &wl_compositor_interface, min(version, preferred)); + } + + else if (streq(interface, wl_subcompositor_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->sub_compositor = wl_registry_bind( + wayl->registry, name, &wl_subcompositor_interface, required); + } + + else if (streq(interface, wl_shm_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + +#if defined(WL_SHM_RELEASE_SINCE_VERSION) + const uint32_t preferred = WL_SHM_RELEASE_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif + + wayl->shm = wl_registry_bind( + wayl->registry, name, &wl_shm_interface, min(version, preferred)); + wl_shm_add_listener(wayl->shm, &shm_listener, wayl); +#if defined(WL_SHM_RELEASE_SINCE_VERSION) + wayl->use_shm_release = version >= WL_SHM_RELEASE_SINCE_VERSION; +#else + wayl->use_shm_release = false; +#endif + } + + else if (streq(interface, xdg_wm_base_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + /* + * We *require* version 1, but _can_ use version 2, 5 or 7, if + * available. + * + * Version 2 adds 'tiled' window states. We use this + * information to restore the window size when window is + * un-tiled. + * + * Version 5 adds 'wm_capabilities'. We use this information + * to draw window decorations. + * + * Version 7 adds 'constrained' window states. We use this + * information to determine whether to allow window resize + * (via CSDs) or not. + */ +#if defined(XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION) + const uint32_t preferred = XDG_TOPLEVEL_STATE_CONSTRAINED_LEFT_SINCE_VERSION; +#elif defined(XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION) + const uint32_t preferred = XDG_TOPLEVEL_WM_CAPABILITIES_SINCE_VERSION; +#elif defined(XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION) + const uint32_t preferred = XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif + + wayl->shell = wl_registry_bind( + wayl->registry, name, &xdg_wm_base_interface, min(version, preferred)); + xdg_wm_base_add_listener(wayl->shell, &xdg_wm_base_listener, wayl); + } + + else if (streq(interface, zxdg_decoration_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->xdg_decoration_manager = wl_registry_bind( + wayl->registry, name, &zxdg_decoration_manager_v1_interface, required); + } + + else if (streq(interface, wl_seat_interface.name)) { + const uint32_t required = 5; + if (!verify_iface_version(interface, version, required)) + return; + +#if defined(WL_POINTER_AXIS_VALUE120_SINCE_VERSION) + const uint32_t preferred = WL_POINTER_AXIS_VALUE120_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif + + int repeat_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (repeat_fd == -1) { + LOG_ERRNO("failed to create keyboard repeat timer FD"); + return; + } + + struct wl_seat *wl_seat = wl_registry_bind( + wayl->registry, name, &wl_seat_interface, min(version, preferred)); + + tll_push_back(wayl->seats, ((struct seat){ + .wayl = wayl, + .wl_seat = wl_seat, + .wl_name = name, + .kbd = { + .repeat = { + .fd = repeat_fd, + }, + }})); + + struct seat *seat = &tll_back(wayl->seats); + + if (!fdm_add(wayl->fdm, repeat_fd, EPOLLIN, &fdm_repeat, seat)) { + close(repeat_fd); + seat->kbd.repeat.fd = -1; + seat_destroy(seat); + return; + } + + seat->kbd.xkb = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + if (seat->kbd.xkb != NULL) { + seat->kbd.xkb_compose_table = xkb_compose_table_new_from_locale( + seat->kbd.xkb, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); + + if (seat->kbd.xkb_compose_table != NULL) { + seat->kbd.xkb_compose_state = xkb_compose_state_new( + seat->kbd.xkb_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); + } else { + LOG_WARN("failed to instantiate compose table; dead keys (compose) will not work"); + } + } + + seat_add_data_device(seat); + seat_add_primary_selection(seat); + seat_add_text_input(seat); + seat_add_key_bindings(seat); + wl_seat_add_listener(wl_seat, &seat_listener, seat); + } + + else if (streq(interface, zxdg_output_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->xdg_output_manager = wl_registry_bind( + wayl->registry, name, &zxdg_output_manager_v1_interface, + min(version, 2)); + + tll_foreach(wayl->monitors, it) { + struct monitor *mon = &it->item; + mon->xdg = zxdg_output_manager_v1_get_xdg_output( + wayl->xdg_output_manager, mon->output); + zxdg_output_v1_add_listener(mon->xdg, &xdg_output_listener, mon); + } + } + + else if (streq(interface, wl_output_interface.name)) { + const uint32_t required = 2; + if (!verify_iface_version(interface, version, required)) + return; + +#if defined(WL_OUTPUT_NAME_SINCE_VERSION) + const uint32_t preferred = WL_OUTPUT_NAME_SINCE_VERSION; +#elif defined(WL_OUTPUT_RELEASE_SINCE_VERSION) + const uint32_t preferred = WL_OUTPUT_RELEASE_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif + + struct wl_output *output = wl_registry_bind( + wayl->registry, name, &wl_output_interface, min(version, preferred)); + + tll_push_back( + wayl->monitors, + ((struct monitor){.wayl = wayl, .output = output, .wl_name = name, + .scale = 1, + .use_output_release = version >= WL_OUTPUT_RELEASE_SINCE_VERSION})); + + struct monitor *mon = &tll_back(wayl->monitors); + wl_output_add_listener(output, &output_listener, mon); + + if (wayl->xdg_output_manager != NULL) { + mon->xdg = zxdg_output_manager_v1_get_xdg_output( + wayl->xdg_output_manager, mon->output); + zxdg_output_v1_add_listener(mon->xdg, &xdg_output_listener, mon); + } + } + + else if (streq(interface, wl_data_device_manager_interface.name)) { + const uint32_t required = 3; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->data_device_manager = wl_registry_bind( + wayl->registry, name, &wl_data_device_manager_interface, required); + + tll_foreach(wayl->seats, it) + seat_add_data_device(&it->item); + } + + else if (streq(interface, zwp_primary_selection_device_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->primary_selection_device_manager = wl_registry_bind( + wayl->registry, name, + &zwp_primary_selection_device_manager_v1_interface, required); + + tll_foreach(wayl->seats, it) + seat_add_primary_selection(&it->item); + } + + else if (streq(interface, wp_presentation_interface.name)) { + if (wayl->presentation_timings) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->presentation = wl_registry_bind( + wayl->registry, name, &wp_presentation_interface, required); + wp_presentation_add_listener( + wayl->presentation, &presentation_listener, wayl); + } + } + + else if (streq(interface, xdg_activation_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->xdg_activation = wl_registry_bind( + wayl->registry, name, &xdg_activation_v1_interface, required); + } + + else if (streq(interface, wp_viewporter_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->viewporter = wl_registry_bind( + wayl->registry, name, &wp_viewporter_interface, required); + } + + else if (streq(interface, wp_fractional_scale_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->fractional_scale_manager = wl_registry_bind( + wayl->registry, name, + &wp_fractional_scale_manager_v1_interface, required); + } + + else if (streq(interface, wp_cursor_shape_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + +#if defined(WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION) /* 1.42 */ + const uint32_t preferred = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DND_ASK_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif + + wayl->shape_manager_version = min(required, preferred); + wayl->cursor_shape_manager = wl_registry_bind( + wayl->registry, name, &wp_cursor_shape_manager_v1_interface, + min(required, preferred)); + } + + else if (streq(interface, wp_single_pixel_buffer_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->single_pixel_manager = wl_registry_bind( + wayl->registry, name, + &wp_single_pixel_buffer_manager_v1_interface, required); + } + + else if (streq(interface, xdg_toplevel_icon_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->toplevel_icon_manager = wl_registry_bind( + wayl->registry, name, &xdg_toplevel_icon_manager_v1_interface, required); + } + + else if (streq(interface, xdg_system_bell_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->system_bell = wl_registry_bind( + wayl->registry, name, &xdg_system_bell_v1_interface, required); + } + + else if (streq(interface, wp_color_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->color_management.manager = wl_registry_bind( + wayl->registry, name, &wp_color_manager_v1_interface, required); + + wp_color_manager_v1_add_listener( + wayl->color_management.manager, &color_manager_listener, wayl); + } + +#if defined(HAVE_XDG_TOPLEVEL_TAG) + else if (streq(interface, xdg_toplevel_tag_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->toplevel_tag_manager = wl_registry_bind( + wayl->registry, name, &xdg_toplevel_tag_manager_v1_interface, required); + } +#endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + else if (streq(interface, ext_background_effect_manager_v1_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->background_effect_manager = wl_registry_bind( + wayl->registry, name, + &ext_background_effect_manager_v1_interface, required); + + ext_background_effect_manager_v1_add_listener( + wayl->background_effect_manager, &background_manager_listener, wayl); + } +#endif + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + else if (streq(interface, zwp_text_input_manager_v3_interface.name)) { + const uint32_t required = 1; + if (!verify_iface_version(interface, version, required)) + return; + + wayl->text_input_manager = wl_registry_bind( + wayl->registry, name, &zwp_text_input_manager_v3_interface, required); + + tll_foreach(wayl->seats, it) + seat_add_text_input(&it->item); + } +#endif + +} + +static void +monitor_destroy(struct monitor *mon) +{ + if (mon->xdg != NULL) + zxdg_output_v1_destroy(mon->xdg); + if (mon->output != NULL) { + if (mon->use_output_release) + wl_output_release(mon->output); + else + wl_output_destroy(mon->output); + } + free(mon->make); + free(mon->model); + free(mon->name); + free(mon->description); +} + +static void +handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) +{ + LOG_DBG("global removed: 0x%08x", name); + + struct wayland *wayl = data; + + /* Check if this is an output */ + tll_foreach(wayl->monitors, it) { + struct monitor *mon = &it->item; + + if (mon->wl_name != name) + continue; + + LOG_INFO("monitor unplugged or disabled: %s", mon->name); + + /* + * Update all terminals that are mapped here. On Sway 1.4, + * surfaces are *not* unmapped before the output is removed + */ + + tll_foreach(wayl->terms, t) { + tll_foreach(t->item->window->on_outputs, o) { + if (o->item->output == mon->output) { + surface_leave(t->item->window, NULL, mon->output); + break; + } + } + } + + monitor_destroy(mon); + tll_remove(wayl->monitors, it); + return; + } + + /* A seat? */ + tll_foreach(wayl->seats, it) { + struct seat *seat = &it->item; + + if (seat->wl_name != name) + continue; + + LOG_INFO("seat destroyed: %s", seat->name); + + if (seat->kbd_focus != NULL) { + LOG_WARN("compositor destroyed seat '%s' " + "without sending a keyboard leave event", + seat->name); + + if (seat->wl_keyboard != NULL) + keyboard_listener.leave( + seat, seat->wl_keyboard, -1, seat->kbd_focus->window->surface.surf); + } + + if (seat->mouse_focus != NULL) { + LOG_WARN("compositor destroyed seat '%s' " + "without sending a pointer leave event", + seat->name); + + if (seat->wl_pointer != NULL) + pointer_listener.leave( + seat, seat->wl_pointer, -1, seat->mouse_focus->window->surface.surf); + } + + seat_destroy(seat); + tll_remove(wayl->seats, it); + return; + } + + LOG_WARN("unknown global removed: 0x%08x", name); +} + +static const struct wl_registry_listener registry_listener = { + .global = &handle_global, + .global_remove = &handle_global_remove, +}; + +static void +fdm_hook(struct fdm *fdm, void *data) +{ + struct wayland *wayl = data; + wayl_flush(wayl); +} + +static bool +fdm_wayl(struct fdm *fdm, int fd, int events, void *data) +{ + struct wayland *wayl = data; + int event_count = 0; + + if (events & EPOLLIN) { + if (wl_display_read_events(wayl->display) < 0) { + LOG_ERRNO("failed to read events from the Wayland socket"); + return false; + } + + wl_display_dispatch_pending(wayl->display); + + while (wl_display_prepare_read(wayl->display) != 0) { + if (wl_display_dispatch_pending(wayl->display) < 0) { + LOG_ERRNO("failed to dispatch pending Wayland events"); + return false; + } + } + } + + if (events & EPOLLHUP) { + LOG_WARN("disconnected from Wayland"); + /* + * Do *not* call wl_display_cancel_read() here. + * + * Doing so causes later calls to wayl_roundtrip() (called + * from term_destroy() -> wayl_win_destroy()) to hang + * indefinitely. + * + * https://codeberg.org/dnkl/foot/issues/651 + */ + return false; + } + + return event_count != -1; +} + +struct wayland * +wayl_init(struct fdm *fdm, struct key_binding_manager *key_binding_manager, + bool presentation_timings) +{ + struct wayland *wayl = calloc(1, sizeof(*wayl)); + if (unlikely(wayl == NULL)) { + LOG_ERRNO("calloc() failed"); + return NULL; + } + + wayl->fdm = fdm; + wayl->key_binding_manager = key_binding_manager; + wayl->fd = -1; + wayl->presentation_timings = presentation_timings; + + if (!fdm_hook_add(fdm, &fdm_hook, wayl, FDM_HOOK_PRIORITY_LOW)) { + LOG_ERR("failed to add FDM hook"); + goto out; + } + + wayl->display = wl_display_connect(NULL); + if (wayl->display == NULL) { + LOG_ERR("failed to connect to wayland; no compositor running?"); + goto out; + } + + wayl->registry = wl_display_get_registry(wayl->display); + if (wayl->registry == NULL) { + LOG_ERR("failed to get wayland registry"); + goto out; + } + + wl_registry_add_listener(wayl->registry, ®istry_listener, wayl); + wl_display_roundtrip(wayl->display); + + if (wayl->compositor == NULL) { + LOG_ERR("no compositor"); + goto out; + } + if (wayl->sub_compositor == NULL) { + LOG_ERR("no sub compositor"); + goto out; + } + if (wayl->shm == NULL) { + LOG_ERR("no shared memory buffers interface"); + goto out; + } + if (wayl->shell == NULL) { + LOG_ERR("no XDG shell interface"); + goto out; + } + if (wayl->data_device_manager == NULL) { + LOG_ERR("no clipboard available " + "(wl_data_device_manager not implemented by server)"); + goto out; + } + if (tll_length(wayl->seats) == 0) { + LOG_ERR("no seats available (wl_seat interface too old?)"); + goto out; + } + if (tll_length(wayl->monitors) == 0) { + LOG_ERR("no monitors available"); + goto out; + } + + if (presentation_timings && wayl->presentation == NULL) { + LOG_ERR("compositor does not implement the presentation time interface"); + goto out; + } + + if (wayl->primary_selection_device_manager == NULL) + LOG_WARN("compositor does not implement the primary selection interface"); + + if (wayl->xdg_activation == NULL) { + LOG_WARN( + "compositor does not implement XDG activation, " + "bell.urgent will fall back to coloring the window margins red"); + } + + if (wayl->fractional_scale_manager == NULL || wayl->viewporter == NULL) + LOG_WARN("compositor does not implement fractional scaling"); + + if (wayl->cursor_shape_manager == NULL) { + LOG_WARN("compositor does not implement server-side cursors, " + "falling back to client-side cursors"); + } + + if (wayl->toplevel_icon_manager == NULL) { + LOG_WARN("compositor does not implement the xdg-toplevel-icon protocol"); + } + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (wayl->text_input_manager == NULL) { + LOG_WARN("text input interface not implemented by compositor; " + "IME will be disabled"); + } +#endif + + /* Trigger listeners registered when handling globals */ + wl_display_roundtrip(wayl->display); + + tll_foreach(wayl->monitors, it) { + LOG_INFO( + "%s: %dx%d+%dx%d@%dHz %s %.2f\" scale=%d, DPI=%.2f/%.2f (physical/scaled)", + it->item.name, it->item.dim.px_real.width, it->item.dim.px_real.height, + it->item.x, it->item.y, (int)roundf(it->item.refresh), + it->item.model != NULL ? it->item.model : it->item.description, + it->item.inch, it->item.scale, + it->item.dpi.physical, it->item.dpi.scaled); + } + + wayl->fd = wl_display_get_fd(wayl->display); + if (fcntl(wayl->fd, F_SETFL, fcntl(wayl->fd, F_GETFL) | O_NONBLOCK) < 0) { + LOG_ERRNO("failed to make Wayland socket non-blocking"); + goto out; + } + + if (!fdm_add(fdm, wayl->fd, EPOLLIN, &fdm_wayl, wayl)) + goto out; + + if (wl_display_prepare_read(wayl->display) != 0) { + LOG_ERRNO("failed to prepare for reading wayland events"); + goto out; + } + + return wayl; + +out: + if (wayl != NULL) + wayl_destroy(wayl); + return NULL; +} + +void +wayl_destroy(struct wayland *wayl) +{ + if (wayl == NULL) + return; + + tll_foreach(wayl->terms, it) { + static bool have_warned = false; + if (!have_warned) { + have_warned = true; + LOG_WARN("there are terminals still running"); + term_destroy(it->item); + } + } + + tll_free(wayl->terms); + + fdm_hook_del(wayl->fdm, &fdm_hook, FDM_HOOK_PRIORITY_LOW); + + tll_foreach(wayl->monitors, it) { + monitor_destroy(&it->item); + tll_remove(wayl->monitors, it); + } + + tll_foreach(wayl->seats, it) { + seat_destroy(&it->item); + tll_remove(wayl->seats, it); + } + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + if (wayl->text_input_manager != NULL) + zwp_text_input_manager_v3_destroy(wayl->text_input_manager); +#endif + +#if defined(HAVE_XDG_TOPLEVEL_TAG) + if (wayl->toplevel_tag_manager != NULL) + xdg_toplevel_tag_manager_v1_destroy(wayl->toplevel_tag_manager); +#endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + if (wayl->background_effect_manager != NULL) + ext_background_effect_manager_v1_destroy(wayl->background_effect_manager); +#endif + + if (wayl->color_management.img_description != NULL) + wp_image_description_v1_destroy(wayl->color_management.img_description); + if (wayl->color_management.manager != NULL) + wp_color_manager_v1_destroy(wayl->color_management.manager); + if (wayl->system_bell != NULL) + xdg_system_bell_v1_destroy(wayl->system_bell); + if (wayl->toplevel_icon_manager != NULL) + xdg_toplevel_icon_manager_v1_destroy(wayl->toplevel_icon_manager); + if (wayl->single_pixel_manager != NULL) + wp_single_pixel_buffer_manager_v1_destroy(wayl->single_pixel_manager); + if (wayl->fractional_scale_manager != NULL) + wp_fractional_scale_manager_v1_destroy(wayl->fractional_scale_manager); + if (wayl->viewporter != NULL) + wp_viewporter_destroy(wayl->viewporter); + if (wayl->cursor_shape_manager != NULL) + wp_cursor_shape_manager_v1_destroy(wayl->cursor_shape_manager); + if (wayl->xdg_activation != NULL) + xdg_activation_v1_destroy(wayl->xdg_activation); + if (wayl->xdg_output_manager != NULL) + zxdg_output_manager_v1_destroy(wayl->xdg_output_manager); + if (wayl->shell != NULL) + xdg_wm_base_destroy(wayl->shell); + if (wayl->xdg_decoration_manager != NULL) + zxdg_decoration_manager_v1_destroy(wayl->xdg_decoration_manager); + if (wayl->presentation != NULL) + wp_presentation_destroy(wayl->presentation); + if (wayl->data_device_manager != NULL) + wl_data_device_manager_destroy(wayl->data_device_manager); + if (wayl->primary_selection_device_manager != NULL) + zwp_primary_selection_device_manager_v1_destroy(wayl->primary_selection_device_manager); + if (wayl->shm != NULL) { +#if defined(WL_SHM_RELEASE_SINCE_VERSION) + if (wayl->use_shm_release) + wl_shm_release(wayl->shm); + else +#endif + wl_shm_destroy(wayl->shm); + } + if (wayl->sub_compositor != NULL) + wl_subcompositor_destroy(wayl->sub_compositor); + if (wayl->compositor != NULL) + wl_compositor_destroy(wayl->compositor); + if (wayl->registry != NULL) + wl_registry_destroy(wayl->registry); + if (wayl->fd != -1) + fdm_del_no_close(wayl->fdm, wayl->fd); + if (wayl->display != NULL) { + wayl_flush(wayl); + wl_display_disconnect(wayl->display); + } + + free(wayl); +} + +static void +fractional_scale_preferred_scale( + void *data, struct wp_fractional_scale_v1 *wp_fractional_scale_v1, + uint32_t scale) +{ + struct wl_window *win = data; + + const float new_scale = (float)scale / 120.; + + if (win->scale == new_scale) + return; + + LOG_DBG("fractional scale: %.2f -> %.2f", win->scale, new_scale); + + win->scale = new_scale; + update_term_for_output_change(win->term); +} + +static const struct wp_fractional_scale_v1_listener fractional_scale_listener = { + .preferred_scale = &fractional_scale_preferred_scale, +}; + +struct wl_window * +wayl_win_init(struct terminal *term, const char *token) +{ + struct wayland *wayl = term->wl; + const struct config *conf = term->conf; + + struct wl_window *win = calloc(1, sizeof(*win)); + if (unlikely(win == NULL)) { + LOG_ERRNO("calloc() failed"); + return NULL; + } + + win->term = term; + win->csd_mode = CSD_UNKNOWN; + win->csd.move_timeout_fd = -1; + win->resize_timeout_fd = -1; + win->scale = -1.; + + win->wm_capabilities.maximize = true; + win->wm_capabilities.minimize = true; + + win->surface.surf = wl_compositor_create_surface(wayl->compositor); + if (win->surface.surf == NULL) { + LOG_ERR("failed to create wayland surface"); + goto out; + } + + wl_surface_add_listener(win->surface.surf, &surface_listener, win); + + if (wayl->fractional_scale_manager != NULL && wayl->viewporter != NULL) { + win->surface.viewport = wp_viewporter_get_viewport(wayl->viewporter, win->surface.surf); + + win->fractional_scale = + wp_fractional_scale_manager_v1_get_fractional_scale( + wayl->fractional_scale_manager, win->surface.surf); + wp_fractional_scale_v1_add_listener( + win->fractional_scale, &fractional_scale_listener, win); + } + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + if (wayl->background_effect_manager != NULL) { + win->surface.background_effect = + ext_background_effect_manager_v1_get_background_effect( + wayl->background_effect_manager, win->surface.surf); + } +#endif + + wayl_win_alpha_changed(win); + + win->xdg_surface = xdg_wm_base_get_xdg_surface(wayl->shell, win->surface.surf); + xdg_surface_add_listener(win->xdg_surface, &xdg_surface_listener, win); + + win->xdg_toplevel = xdg_surface_get_toplevel(win->xdg_surface); + xdg_toplevel_add_listener(win->xdg_toplevel, &xdg_toplevel_listener, win); + + xdg_toplevel_set_app_id(win->xdg_toplevel, conf->app_id); + +#if defined(HAVE_XDG_TOPLEVEL_TAG) + if (conf->toplevel_tag != NULL && conf->toplevel_tag[0] != '\0') { + if (wayl->toplevel_tag_manager != NULL) { + xdg_toplevel_tag_manager_v1_set_toplevel_tag( + wayl->toplevel_tag_manager, win->xdg_toplevel, conf->toplevel_tag); + + /* TODO: the description is recommended to be the tag, but translated */ + xdg_toplevel_tag_manager_v1_set_toplevel_description( + wayl->toplevel_tag_manager, win->xdg_toplevel, conf->toplevel_tag); + } else { + LOG_WARN("compositor does not implement the xdg-toplevel-tag protocol"); + } + } +#endif + + if (wayl->toplevel_icon_manager != NULL) { + const char *app_id = + term->app_id != NULL ? term->app_id : term->conf->app_id; + + struct xdg_toplevel_icon_v1 *icon = + xdg_toplevel_icon_manager_v1_create_icon(wayl->toplevel_icon_manager); + xdg_toplevel_icon_v1_set_name(icon, streq( + app_id, "footclient") ? "foot" : app_id); + xdg_toplevel_icon_manager_v1_set_icon( + wayl->toplevel_icon_manager, win->xdg_toplevel, icon); + xdg_toplevel_icon_v1_destroy(icon); + } + + if (term->conf->gamma_correct) { + if (wayl->color_management.img_description != NULL) { + xassert(wayl->color_management.manager != NULL); + + win->surface.color_management = wp_color_manager_v1_get_surface( + term->wl->color_management.manager, win->surface.surf); + + wp_color_management_surface_v1_set_image_description( + win->surface.color_management, wayl->color_management.img_description, + WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); + } else { + if (wayl->color_management.manager == NULL) { + LOG_WARN( + "gamma-corrected-blending: disabling; " + "compositor does not implement the color-management protocol"); + } else { + LOG_WARN( + "gamma-corrected-blending: disabling; " + "compositor does not implement all required color-management features"); + LOG_WARN("use e.g. 'wayland-info' and verify the compositor implements:"); + LOG_WARN(" - feature: parametric"); + LOG_WARN(" - render intent: perceptual"); + LOG_WARN(" - TF: ext_linear"); + LOG_WARN(" - primaries: sRGB"); + } + } + } + + if (conf->csd.preferred == CONF_CSD_PREFER_NONE) { + /* User specifically do *not* want decorations */ + win->csd_mode = CSD_NO; + LOG_INFO("window decorations disabled by user"); + } else if (wayl->xdg_decoration_manager != NULL) { + win->xdg_toplevel_decoration = zxdg_decoration_manager_v1_get_toplevel_decoration( + wayl->xdg_decoration_manager, win->xdg_toplevel); + + LOG_INFO("requesting %s decorations", + conf->csd.preferred == CONF_CSD_PREFER_SERVER ? "SSD" : "CSD"); + + zxdg_toplevel_decoration_v1_set_mode( + win->xdg_toplevel_decoration, + (conf->csd.preferred == CONF_CSD_PREFER_SERVER + ? ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE + : ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE)); + + zxdg_toplevel_decoration_v1_add_listener( + win->xdg_toplevel_decoration, &xdg_toplevel_decoration_listener, win); + } else { + /* No decoration manager - thus we *must* draw our own decorations */ + win->configure.csd_mode = CSD_YES; + LOG_WARN("no decoration manager available - using CSDs unconditionally"); + } + + wl_surface_commit(win->surface.surf); + + /* Complete XDG startup notification */ + wayl_activate(wayl, win, token); + + if (!wayl_win_subsurface_new(win, &win->overlay, false)) { + LOG_ERR("failed to create overlay surface"); + goto out; + } + + if (conf->tabs.enabled) { + if (!wayl_win_subsurface_new(win, &win->tab_bar, true)) { + LOG_ERR("failed to create tab bar surface"); + goto out; + } + wl_subsurface_set_desync(win->tab_bar.sub); + + if (!wayl_win_subsurface_new(win, &win->tab_overview, true)) { + LOG_ERR("failed to create tab overview surface"); + goto out; + } + /* desync so commits aren't blocked on parent surface */ + wl_subsurface_set_desync(win->tab_overview.sub); + } + + /* Initialize tab list with primary terminal */ + win->tabs[0] = term; + win->tab_count = 1; + win->active_tab = 0; + win->tab_overview_state.hover_idx = -1; + + switch (conf->tweak.render_timer) { + case RENDER_TIMER_OSD: + case RENDER_TIMER_BOTH: + if (!wayl_win_subsurface_new(win, &win->render_timer, false)) { + LOG_ERR("failed to create render timer surface"); + goto out; + } + break; + + case RENDER_TIMER_NONE: + case RENDER_TIMER_LOG: + break; + } + + return win; + +out: + if (win != NULL) + wayl_win_destroy(win); + return NULL; +} + +void +wayl_win_destroy(struct wl_window *win) +{ + if (win == NULL) + return; + + struct terminal *term = win->term; + + if (win->csd.move_timeout_fd != -1) + close(win->csd.move_timeout_fd); + + /* + * First, unmap all surfaces to trigger things like + * keyboard_leave() and wl_pointer_leave(). + * + * This ensures we remove all references to *this* window from the + * global wayland struct (since it no longer has neither keyboard + * nor mouse focus). + */ + + if (win->render_timer.surface.surf != NULL) { + wl_surface_attach(win->render_timer.surface.surf, NULL, 0, 0); + wl_surface_commit(win->render_timer.surface.surf); + } + + if (win->scrollback_indicator.surface.surf != NULL) { + wl_surface_attach(win->scrollback_indicator.surface.surf, NULL, 0, 0); + wl_surface_commit(win->scrollback_indicator.surface.surf); + } + + /* Scrollback search */ + if (win->search.surface.surf != NULL) { + wl_surface_attach(win->search.surface.surf, NULL, 0, 0); + wl_surface_commit(win->search.surface.surf); + } + + /* Tab bar */ + if (win->tab_bar.surface.surf != NULL) { + wl_surface_attach(win->tab_bar.surface.surf, NULL, 0, 0); + wl_surface_commit(win->tab_bar.surface.surf); + } + + /* Tab overview */ + if (win->tab_overview.surface.surf != NULL) { + wl_surface_attach(win->tab_overview.surface.surf, NULL, 0, 0); + wl_surface_commit(win->tab_overview.surface.surf); + } + + /* URLs */ + tll_foreach(win->urls, it) { + wl_surface_attach(it->item.surf.surface.surf, NULL, 0, 0); + wl_surface_commit(it->item.surf.surface.surf); + } + + /* CSD */ + for (size_t i = 0; i < ALEN(win->csd.surface); i++) { + if (win->csd.surface[i].surface.surf != NULL) { + wl_surface_attach(win->csd.surface[i].surface.surf, NULL, 0, 0); + wl_surface_commit(win->csd.surface[i].surface.surf); + } + } + + wayl_roundtrip(win->term->wl); + + /* Main window */ + win->unmapped = true; + wl_surface_attach(win->surface.surf, NULL, 0, 0); + wl_surface_commit(win->surface.surf); + wayl_roundtrip(win->term->wl); + + tll_free(win->on_outputs); + + tll_foreach(win->urls, it) { + wayl_win_subsurface_destroy(&it->item.surf); + tll_remove(win->urls, it); + } + + render_wait_for_preapply_damage(term); + + csd_destroy(win); + wayl_win_subsurface_destroy(&win->search); + wayl_win_subsurface_destroy(&win->scrollback_indicator); + wayl_win_subsurface_destroy(&win->render_timer); + wayl_win_subsurface_destroy(&win->overlay); + wayl_win_subsurface_destroy(&win->tab_bar); + wayl_win_subsurface_destroy(&win->tab_overview); + + shm_purge(term->render.chains.search); + shm_purge(term->render.chains.scrollback_indicator); + shm_purge(term->render.chains.render_timer); + shm_purge(term->render.chains.grid); + shm_purge(term->render.chains.url); + shm_purge(term->render.chains.csd); + shm_purge(term->render.chains.tab_bar); + shm_purge(term->render.chains.tab_overview); + + tll_foreach(win->xdg_tokens, it) { + xdg_activation_token_v1_destroy(it->item->xdg_token); + free(it->item); + + tll_remove(win->xdg_tokens, it); + } + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + if (win->surface.background_effect != NULL) + ext_background_effect_surface_v1_destroy(win->surface.background_effect); +#endif + + if (win->surface.color_management != NULL) + wp_color_management_surface_v1_destroy(win->surface.color_management); + if (win->fractional_scale != NULL) + wp_fractional_scale_v1_destroy(win->fractional_scale); + if (win->surface.viewport != NULL) + wp_viewport_destroy(win->surface.viewport); + if (win->frame_callback != NULL) + wl_callback_destroy(win->frame_callback); + if (win->xdg_toplevel_decoration != NULL) + zxdg_toplevel_decoration_v1_destroy(win->xdg_toplevel_decoration); + if (win->xdg_toplevel != NULL) + xdg_toplevel_destroy(win->xdg_toplevel); + if (win->xdg_surface != NULL) + xdg_surface_destroy(win->xdg_surface); + if (win->surface.surf != NULL) + wl_surface_destroy(win->surface.surf); + + wayl_roundtrip(win->term->wl); + + if (win->resize_timeout_fd >= 0) + fdm_del(win->term->wl->fdm, win->resize_timeout_fd); + free(win); +} + +bool +wayl_reload_xcursor_theme(struct seat *seat, float new_scale) +{ + if (seat->pointer.theme != NULL && seat->pointer.scale == new_scale) { + /* We already have a theme loaded, and the scale hasn't changed */ + return true; + } + + if (seat->pointer.theme != NULL) { + xassert(seat->pointer.scale != new_scale); + wl_cursor_theme_destroy(seat->pointer.theme); + seat->pointer.theme = NULL; + seat->pointer.cursor = NULL; + } + + if (seat->pointer.shape_device != NULL) { + /* Using server side cursors */ + return true; + } + + int xcursor_size = 24; + + { + const char *env_cursor_size = getenv("XCURSOR_SIZE"); + if (env_cursor_size != NULL) { + errno = 0; + char *end; + int size = (int)strtol(env_cursor_size, &end, 10); + + if (errno == 0 && *end == '\0' && size > 0) + xcursor_size = size; + else + LOG_WARN("XCURSOR_SIZE '%s' is invalid, defaulting to 24", + env_cursor_size); + } + } + + const char *xcursor_theme = getenv("XCURSOR_THEME"); + + LOG_INFO("cursor theme: %s, size: %d, scale: %.2f", + xcursor_theme ? xcursor_theme : "(null)", + xcursor_size, new_scale); + + seat->pointer.theme = wl_cursor_theme_load( + xcursor_theme, xcursor_size * new_scale, seat->wayl->shm); + + if (seat->pointer.theme == NULL) { + LOG_ERR("failed to load cursor theme"); + return false; + } + + seat->pointer.scale = new_scale; + return true; +} + +void +wayl_flush(struct wayland *wayl) +{ + while (true) { + int r = wl_display_flush(wayl->display); + if (r >= 0) { + /* Most likely code path - the flush succeed */ + return; + } + + if (errno == EINTR) { + /* Unlikely */ + continue; + } + + if (errno != EAGAIN) { + const int saved_errno = errno; + + if (errno == EPIPE) { + wl_display_read_events(wayl->display); + wl_display_dispatch_pending(wayl->display); + } + + LOG_ERRNO_P(saved_errno, "failed to flush wayland socket"); + return; + } + + /* Socket buffer is full - need to wait for it to become + writeable again */ + xassert(errno == EAGAIN); + + while (true) { + struct pollfd fds[] = {{.fd = wayl->fd, .events = POLLOUT}}; + + r = poll(fds, sizeof(fds) / sizeof(fds[0]), -1); + + if (r < 0) { + if (errno == EINTR) + continue; + + LOG_ERRNO("failed to poll"); + return; + } + + if (fds[0].revents & POLLHUP) + return; + + xassert(fds[0].revents & POLLOUT); + break; + } + } +} + +void +wayl_roundtrip(struct wayland *wayl) +{ + wl_display_cancel_read(wayl->display); + if (wl_display_roundtrip(wayl->display) < 0) { + LOG_ERRNO("failed to roundtrip Wayland display"); + return; + } + + /* I suspect the roundtrip above clears the pending queue, and + * that prepare_read() will always succeed in the first call. But, + * better safe than sorry... */ + + while (wl_display_prepare_read(wayl->display) != 0) { + if (wl_display_dispatch_pending(wayl->display) < 0) { + LOG_ERRNO("failed to dispatch pending Wayland events"); + return; + } + } + wayl_flush(wayl); +} + +static void +surface_scale_explicit_width_height( + const struct wl_window *win, const struct wayl_surface *surf, + int width, int height, float scale, bool verify) +{ + if (term_fractional_scaling(win->term)) { + LOG_DBG("scaling by a factor of %.2f using fractional scaling " + "(width=%d, height=%d) ", scale, width, height); + + if (verify) { + if ((int)roundf(scale * (int)roundf(width / scale)) != width) { + BUG("width=%d is not valid with scaling factor %.2f (%d != %d)", + width, scale, + (int)roundf(scale * (int)roundf(width / scale)), + width); + } + + if ((int)roundf(scale * (int)roundf(height / scale)) != height) { + BUG("height=%d is not valid with scaling factor %.2f (%d != %d)", + height, scale, + (int)roundf(scale * (int)roundf(height / scale)), + height); + } + } + + xassert(surf->viewport != NULL); + wl_surface_set_buffer_scale(surf->surf, 1); + wp_viewport_set_destination( + surf->viewport, roundf(width / scale), roundf(height / scale)); + } else { + const char *mode UNUSED = term_preferred_buffer_scale(win->term) + ? "wl_surface.preferred_buffer_scale" + : "legacy mode"; + LOG_DBG("scaling by a factor of %.2f using %s " + "(width=%d, height=%d)" , scale, mode, width, height); + + xassert(scale == floorf(scale)); + const int iscale = (int)floorf(scale); + + if (verify) { + if (width % iscale != 0) { + BUG("width=%d is not valid with scaling factor %.2f (%d %% %d != 0)", + width, scale, width, iscale); + } + + if (height % iscale != 0) { + BUG("height=%d is not valid with scaling factor %.2f (%d %% %d != 0)", + height, scale, height, iscale); + } + } + + wl_surface_set_buffer_scale(surf->surf, iscale); + } +} + +void +wayl_surface_scale_explicit_width_height( + const struct wl_window *win, const struct wayl_surface *surf, + int width, int height, float scale) +{ + surface_scale_explicit_width_height(win, surf, width, height, scale, false); +} + +void +wayl_surface_scale(const struct wl_window *win, const struct wayl_surface *surf, + const struct buffer *buf, float scale) +{ + surface_scale_explicit_width_height( + win, surf, buf->width, buf->height, scale, true); +} + +void +wayl_win_scale(struct wl_window *win, const struct buffer *buf) +{ + const struct terminal *term = win->term; + const float scale = term->scale; + + wayl_surface_scale(win, &win->surface, buf, scale); +} + +void +wayl_win_alpha_changed(struct wl_window *win) +{ + struct terminal *term = win->term; + struct wayland *wayl = term->wl; + + /* + * When fullscreened, transparency is disabled (see render.c). + * Update the opaque region to match. + */ + const bool is_opaque = term->colors.alpha == 0xffff || win->is_fullscreen; + + if (is_opaque) { + struct wl_region *region = wl_compositor_create_region(wayl->compositor); + + if (region != NULL) { + wl_region_add(region, 0, 0, INT32_MAX, INT32_MAX); + wl_surface_set_opaque_region(win->surface.surf, region); + wl_region_destroy(region); + } + } else + wl_surface_set_opaque_region(win->surface.surf, NULL); + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + if (term_theme_get(term)->blur) { + if (wayl->have_background_blur) { + xassert(win->surface.background_effect != NULL); + + if (is_opaque) { + /* No transparency, disable blur */ + LOG_DBG("disabling background blur"); + ext_background_effect_surface_v1_set_blur_region( + win->surface.background_effect, NULL); + } else { + /* We have transparency, enable blur if user has enabled it */ + struct wl_region *region = wl_compositor_create_region(wayl->compositor); + if (region != NULL) { + LOG_DBG("enabling background blur"); + + wl_region_add(region, 0, 0, INT32_MAX, INT32_MAX); + ext_background_effect_surface_v1_set_blur_region( + win->surface.background_effect, region); + wl_region_destroy(region); + } + } + } else { + static bool have_warned = false; + if (!have_warned) { + LOG_WARN("background blur requested, but compositor does not support it"); + have_warned = true; + } + } + } +#endif /* HAVE_EXT_BACKGROUND_EFFECT */ +} + +static void +activation_token_for_urgency_done(const char *token, void *data) +{ + struct wl_window *win = data; + struct wayland *wayl = win->term->wl; + + win->urgency_token_is_pending = false; + xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); +} + +bool +wayl_win_set_urgent(struct wl_window *win) +{ + if (win->urgency_token_is_pending) { + /* We already have a pending token. Don't request another one, + * to avoid flooding the Wayland socket */ + return true; + } + + bool success = wayl_get_activation_token( + win->term->wl, NULL, 0, win, &activation_token_for_urgency_done, win); + + if (success) { + win->urgency_token_is_pending = true; + return true; + } + + return false; +} + +bool +wayl_win_ring_bell(const struct wl_window *win) +{ + if (win->term->wl->system_bell == NULL) { + static bool have_warned = false; + + if (!have_warned) { + LOG_WARN("compositor does not implement the XDG system bell protocol"); + have_warned = true; + } + + return false; + } + + xdg_system_bell_v1_ring(win->term->wl->system_bell, win->surface.surf); + return true; +} + +bool +wayl_win_csd_titlebar_visible(const struct wl_window *win) +{ + return win->csd_mode == CSD_YES && + !win->is_fullscreen && + !(win->is_maximized && win->term->conf->csd.hide_when_maximized); +} + +bool +wayl_win_csd_borders_visible(const struct wl_window *win) +{ + return win->csd_mode == CSD_YES && + !win->is_fullscreen && + !win->is_maximized; +} + +bool +wayl_win_subsurface_new_with_custom_parent( + struct wl_window *win, struct wl_surface *parent, + struct wayl_sub_surface *surf, bool allow_pointer_input) +{ + struct wayland *wayl = win->term->wl; + + surf->surface.surf = NULL; + surf->surface.viewport = NULL; + surf->sub = NULL; + + struct wl_surface *main_surface + = wl_compositor_create_surface(wayl->compositor); + + if (main_surface == NULL) { + LOG_ERR("failed to instantiate surface for sub-surface"); + return false; + } + + surf->surface.color_management = NULL; + if (win->term->conf->gamma_correct && + wayl->color_management.img_description != NULL) + { + xassert(wayl->color_management.manager != NULL); + + surf->surface.color_management = wp_color_manager_v1_get_surface( + wayl->color_management.manager, main_surface); + + wp_color_management_surface_v1_set_image_description( + surf->surface.color_management, wayl->color_management.img_description, + WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); + } + + struct wl_subsurface *sub = wl_subcompositor_get_subsurface( + wayl->sub_compositor, main_surface, parent); + + if (sub == NULL) { + LOG_ERR("failed to instantiate sub-surface"); + wl_surface_destroy(main_surface); + return false; + } + + struct wp_viewport *viewport = NULL; + if (wayl->viewporter != NULL) { + viewport = wp_viewporter_get_viewport(wayl->viewporter, main_surface); + if (viewport == NULL) { + LOG_ERR("failed to instantiate viewport for sub-surface"); + wl_subsurface_destroy(sub); + wl_surface_destroy(main_surface); + return false; + } + } + + wl_surface_set_user_data(main_surface, win); + wl_subsurface_set_sync(sub); + + /* Disable pointer and touch events */ + if (!allow_pointer_input) { + struct wl_region *empty = + wl_compositor_create_region(wayl->compositor); + wl_surface_set_input_region(main_surface, empty); + wl_region_destroy(empty); + } + + surf->surface.surf = main_surface; + surf->sub = sub; + surf->surface.viewport = viewport; + return true; +} + +bool +wayl_win_subsurface_new(struct wl_window *win, struct wayl_sub_surface *surf, + bool allow_pointer_input) +{ + return wayl_win_subsurface_new_with_custom_parent( + win, win->surface.surf, surf, allow_pointer_input); +} + +void +wayl_win_subsurface_destroy(struct wayl_sub_surface *surf) +{ + if (surf == NULL) + return; + + if (surf->surface.color_management != NULL) { + wp_color_management_surface_v1_destroy(surf->surface.color_management); + surf->surface.color_management = NULL; + } + + if (surf->surface.viewport != NULL) { + wp_viewport_destroy(surf->surface.viewport); + surf->surface.viewport = NULL; + } + + if (surf->sub != NULL) { + wl_subsurface_destroy(surf->sub); + surf->sub = NULL; + } + if (surf->surface.surf != NULL) { + wl_surface_destroy(surf->surface.surf); + surf->surface.surf = NULL; + } +} + +static void +activation_token_done(void *data, struct xdg_activation_token_v1 *xdg_token, + const char *token) +{ + LOG_DBG("XDG activation token done: %s", token); + + struct xdg_activation_token_context *ctx = data; + struct wl_window *win = ctx->win; + + ctx->cb(token, ctx->cb_data); + + tll_foreach(win->xdg_tokens, it) { + if (it->item->xdg_token != xdg_token) + continue; + + xassert(win == it->item->win); + + free(ctx); + xdg_activation_token_v1_destroy(xdg_token); + tll_remove(win->xdg_tokens, it); + return; + } + + BUG("activation token not found in list"); +} + +static const struct +xdg_activation_token_v1_listener activation_token_listener = { + .done = &activation_token_done, +}; + +bool +wayl_get_activation_token( + struct wayland *wayl, struct seat *seat, uint32_t serial, + struct wl_window *win, + void (*cb)(const char *token, void *data), void *cb_data) +{ + if (wayl->xdg_activation == NULL) + return false; + + struct xdg_activation_token_v1 *token = + xdg_activation_v1_get_activation_token(wayl->xdg_activation); + + if (token == NULL) { + LOG_ERR("failed to retrieve XDG activation token"); + return false; + } + + struct xdg_activation_token_context *ctx = xmalloc(sizeof(*ctx)); + *ctx = (struct xdg_activation_token_context){ + .win = win, + .xdg_token = token, + .cb = cb, + .cb_data = cb_data, + }; + tll_push_back(win->xdg_tokens, ctx); + + if (seat != NULL && serial != 0) + xdg_activation_token_v1_set_serial(token, serial, seat->wl_seat); + + xdg_activation_token_v1_set_surface(token, win->surface.surf); + xdg_activation_token_v1_add_listener(token, &activation_token_listener, ctx); + xdg_activation_token_v1_commit(token); + return true; +} + +void +wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token) +{ + if (wayl->xdg_activation == NULL) + return; + + if (token == NULL) + return; + + xdg_activation_v1_activate(wayl->xdg_activation, token, win->surface.surf); +} + +bool +wayl_do_linear_blending(const struct wayland *wayl, const struct config *conf) +{ + return conf->gamma_correct && + wayl->color_management.img_description != NULL; +} diff --git a/wayland.h b/wayland.h new file mode 100644 index 0000000..ef1cf54 --- /dev/null +++ b/wayland.h @@ -0,0 +1,598 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +/* Wayland protocols */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(HAVE_XDG_TOPLEVEL_TAG) + #include +#endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + #include +#endif + +#include +#include + +#include "config.h" +#include "cursor-shape.h" +#include "fdm.h" + +/* Forward declarations */ +struct terminal; +struct buffer; + +/* Mime-types we support when dealing with data offers (e.g. copy-paste, or DnD) */ +enum data_offer_mime_type { + DATA_OFFER_MIME_UNSET, + DATA_OFFER_MIME_TEXT_PLAIN, + DATA_OFFER_MIME_TEXT_UTF8, + DATA_OFFER_MIME_URI_LIST, + + DATA_OFFER_MIME_TEXT_TEXT, + DATA_OFFER_MIME_TEXT_STRING, + DATA_OFFER_MIME_TEXT_UTF8_STRING, +}; + +enum touch_state { + TOUCH_STATE_INHIBITED = -1, + TOUCH_STATE_IDLE, + TOUCH_STATE_HELD, + TOUCH_STATE_DRAGGING, + TOUCH_STATE_SCROLLING, +}; + +struct wayl_surface { + struct wl_surface *surf; + struct wp_viewport *viewport; + struct wp_color_management_surface_v1 *color_management; + +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + struct ext_background_effect_surface_v1 *background_effect; +#endif +}; + +struct wayl_sub_surface { + struct wayl_surface surface; + struct wl_subsurface *sub; +}; + +struct wl_window; +struct wl_clipboard { + struct wl_window *window; /* For DnD */ + struct wl_data_source *data_source; + struct wl_data_offer *data_offer; + enum data_offer_mime_type mime_type; + char *text; + uint32_t serial; +}; + +struct wl_primary { + struct zwp_primary_selection_source_v1 *data_source; + struct zwp_primary_selection_offer_v1 *data_offer; + enum data_offer_mime_type mime_type; + char *text; + uint32_t serial; +}; + +/* Maps a mouse button to its "owning" surface */ +struct button_tracker { + int button; + int surf_kind; /* TODO: this is really an "enum term_surface" */ + bool send_to_client; /* Only valid when surface is the main grid surface */ +}; + +struct rect { + int x; + int y; + int width; + int height; +}; + +struct seat { + struct wayland *wayl; + struct wl_seat *wl_seat; + uint32_t wl_name; + char *name; + + /* Focused terminals */ + struct terminal *kbd_focus; + struct terminal *mouse_focus; + struct terminal *ime_focus; + + /* Keyboard state */ + struct wl_keyboard *wl_keyboard; + struct { + uint32_t serial; + + struct xkb_context *xkb; + struct xkb_keymap *xkb_keymap; + struct xkb_state *xkb_state; + struct xkb_compose_table *xkb_compose_table; + struct xkb_compose_state *xkb_compose_state; + struct { + int fd; + + bool dont_re_repeat; + int32_t delay; + int32_t rate; + uint32_t key; + } repeat; + + xkb_mod_index_t mod_shift; + xkb_mod_index_t mod_alt; + xkb_mod_index_t mod_ctrl; + xkb_mod_index_t mod_super; + xkb_mod_index_t mod_caps; + xkb_mod_index_t mod_num; + + xkb_mod_mask_t legacy_significant; /* Significant modifiers for the legacy keyboard protocol */ + xkb_mod_mask_t kitty_significant; /* Significant modifiers for the kitty keyboard protocol */ + + xkb_mod_mask_t virtual_modifiers; /* Set of modifiers to completely ignore */ + + xkb_keycode_t key_arrow_up; + xkb_keycode_t key_arrow_down; + + /* Enabled modifiers */ + bool shift; + bool alt; + bool ctrl; + bool super; + + xkb_keysym_t last_shortcut_sym; + } kbd; + + /* Pointer state */ + struct wl_pointer *wl_pointer; + struct { + uint32_t serial; + + /* Client-side cursor */ + struct wayl_surface surface; + struct wl_cursor_theme *theme; + struct wl_cursor *cursor; + + /* Server-side cursor */ + struct wp_cursor_shape_device_v1 *shape_device; + + float scale; + bool hidden; + enum cursor_shape shape; + char *last_custom_xcursor; + + struct wl_callback *xcursor_callback; + bool xcursor_pending; + } pointer; + + /* Touch state */ + struct wl_touch *wl_touch; + struct { + enum touch_state state; + + uint32_t serial; + uint32_t time; + struct wl_surface *surface; + int surface_kind; + int32_t id; + } touch; + + struct { + int x; + int y; + int col; + int row; + + /* Mouse buttons currently being pressed, and their "owning" surfaces */ + tll(struct button_tracker) buttons; + + /* Double- and triple click state */ + int count; + int last_released_button; + struct timespec last_time; + + /* We used a discrete axis event in the current pointer frame */ + double aggregated[2]; + double aggregated_120[2]; + bool have_discrete; + } mouse; + + /* Clipboard */ + struct wl_data_device *data_device; + struct zwp_primary_selection_device_v1 *primary_selection_device; + + struct wl_clipboard clipboard; + struct wl_primary primary; + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + /* Input Method Editor */ + struct zwp_text_input_v3 *wl_text_input; + struct { + struct { + struct rect pending; + struct rect sent; + } cursor_rect; + + struct { + struct { + char *text; + int32_t cursor_begin; + int32_t cursor_end; + } pending; + + char32_t *text; + struct cell *cells; + int count; + struct { + bool hidden; + int start; /* Cell index, inclusive */ + int end; /* Cell index, exclusive */ + } cursor; + } preedit; + + struct { + struct { + char *text; + } pending; + } commit; + + struct { + struct { + uint32_t before_length; + uint32_t after_length; + } pending; + } surrounding; + + uint32_t serial; + } ime; +#endif +}; + +enum csd_surface { + CSD_SURF_TITLE, + CSD_SURF_LEFT, + CSD_SURF_RIGHT, + CSD_SURF_TOP, + CSD_SURF_BOTTOM, + CSD_SURF_MINIMIZE, + CSD_SURF_MAXIMIZE, + CSD_SURF_CLOSE, + CSD_SURF_COUNT, +}; + +struct monitor { + struct wayland *wayl; + struct wl_output *output; + struct zxdg_output_v1 *xdg; + uint32_t wl_name; + + int x; + int y; + + struct { + /* Physical size, in mm */ + struct { + int width; + int height; + } mm; + + /* Physical size, in pixels */ + struct { + int width; + int height; + } px_real; + + /* Scaled size, in pixels */ + struct { + int width; + int height; + } px_scaled; + } dim; + + struct { + /* PPI, based on physical size */ + struct { + int x; + int y; + } real; + + /* PPI, logical, based on scaled size */ + struct { + int x; + int y; + } scaled; + } ppi; + + struct { + float scaled; + float physical; + } dpi; + + int scale; + float refresh; + enum wl_output_subpixel subpixel; + enum wl_output_transform transform; + + /* From wl_output */ + char *make; + char *model; + + /* From xdg_output */ + char *name; + char *description; + + float inch; /* e.g. 24" */ + + bool use_output_release; +}; + +struct wl_url { + const struct url *url; + struct wayl_sub_surface surf; +}; + +enum csd_mode {CSD_UNKNOWN, CSD_NO, CSD_YES}; + +typedef void (*activation_token_cb_t)(const char *token, void *data); + +/* + * This context holds data used both in the token::done callback, and + * when cleaning up created, by not-yet-done tokens in + * wayl_win_destroy(). + */ +struct xdg_activation_token_context { + struct wl_window *win; /* Need for win->xdg_tokens */ + struct xdg_activation_token_v1 *xdg_token; /* Used to match token in done() */ + activation_token_cb_t cb; /* User provided callback */ + void *cb_data; /* Callback user pointer */ +}; + +struct wayland; +struct wl_window { + struct terminal *term; + struct wayl_surface surface; + struct xdg_surface *xdg_surface; + struct xdg_toplevel *xdg_toplevel; + struct wp_fractional_scale_v1 *fractional_scale; + + tll(struct xdg_activation_token_context *) xdg_tokens; + bool urgency_token_is_pending; + + bool unmapped; + float scale; + int preferred_buffer_scale; + + struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; + + enum csd_mode csd_mode; + + struct { + struct wayl_sub_surface surface[CSD_SURF_COUNT]; + struct fcft_font *font; + int move_timeout_fd; + uint32_t serial; + } csd; + + struct { + bool maximize:1; + bool minimize:1; + } wm_capabilities; + + struct wayl_sub_surface search; + struct wayl_sub_surface scrollback_indicator; + struct wayl_sub_surface render_timer; + struct wayl_sub_surface overlay; + struct wayl_sub_surface tab_bar; + struct wayl_sub_surface tab_overview; + + /* Tab management */ +#define TAB_MAX 32 + struct terminal *tabs[TAB_MAX]; + size_t tab_count; + size_t active_tab; + + /* Tab bar geometry cache, filled by render_tab_bar, read by hit-testing */ + struct { + int xs[TAB_MAX]; + int ws[TAB_MAX]; + int y; + int h; + size_t count; + } tab_layout; + + /* Tab overview state. progress 0=closed, 1=open; target is what we + * animate toward. Coordinates here are in buffer pixels. */ + struct { + bool target_open; /* user wants it open */ + bool visible; /* sub-surface is currently committed (not hidden) */ + float progress; /* 0.0..1.0 eased animation value */ + uint64_t anim_start_ns; /* CLOCK_MONOTONIC, when animation began */ + float anim_from; /* progress value at anim_start */ + size_t sel_idx; /* keyboard-selected card */ + int hover_idx; /* -1 if no hover, else card index */ + /* Card geometry cache (buffer pixels), filled by render_tab_overview */ + struct { + int x, y, w, h; /* card outer rect */ + } cards[TAB_MAX]; + size_t card_count; + } tab_overview_state; + + struct wl_callback *frame_callback; + + tll(const struct monitor *) on_outputs; /* Outputs we're mapped on */ + tll(struct wl_url) urls; + + bool is_configured; + bool is_fullscreen; + bool is_maximized; + bool is_resizing; + bool is_tiled_top; + bool is_tiled_bottom; + bool is_tiled_left; + bool is_tiled_right; + bool is_tiled; /* At least one of is_tiled_{top,bottom,left,right} is true */ + + bool is_constrained_top; + bool is_constrained_bottom; + bool is_constrained_left; + bool is_constrained_right; + + struct { + int width; + int height; + bool is_activated:1; + bool is_fullscreen:1; + bool is_maximized:1; + bool is_resizing:1; + + bool is_tiled_top:1; + bool is_tiled_bottom:1; + bool is_tiled_left:1; + bool is_tiled_right:1; + + bool is_constrained_top:1; + bool is_constrained_bottom:1; + bool is_constrained_left:1; + bool is_constrained_right:1; + + enum csd_mode csd_mode; + } configure; + + int resize_timeout_fd; +}; + +struct terminal; +struct wayland { + struct fdm *fdm; + struct key_binding_manager *key_binding_manager; + + int fd; + + struct wl_display *display; + struct wl_registry *registry; + struct wl_compositor *compositor; + struct wl_subcompositor *sub_compositor; + struct wl_shm *shm; + + struct zxdg_output_manager_v1 *xdg_output_manager; + + struct xdg_wm_base *shell; + struct zxdg_decoration_manager_v1 *xdg_decoration_manager; + + struct wl_data_device_manager *data_device_manager; + struct zwp_primary_selection_device_manager_v1 *primary_selection_device_manager; + + struct xdg_activation_v1 *xdg_activation; + + struct wp_viewporter *viewporter; + struct wp_fractional_scale_manager_v1 *fractional_scale_manager; + + struct wp_cursor_shape_manager_v1 *cursor_shape_manager; + int shape_manager_version; + + struct wp_single_pixel_buffer_manager_v1 *single_pixel_manager; + + struct xdg_toplevel_icon_manager_v1 *toplevel_icon_manager; + + struct xdg_system_bell_v1 *system_bell; + + struct { + struct wp_color_manager_v1 *manager; + struct wp_image_description_v1 *img_description; + bool have_intent_perceptual; + bool have_feat_parametric; + bool have_tf_ext_linear; + bool have_primaries_srgb; + } color_management; + + bool presentation_timings; + struct wp_presentation *presentation; + uint32_t presentation_clock_id; + +#if defined(HAVE_XDG_TOPLEVEL_TAG) + struct xdg_toplevel_tag_manager_v1 *toplevel_tag_manager; +#endif +#if defined(HAVE_EXT_BACKGROUND_EFFECT) + struct ext_background_effect_manager_v1 *background_effect_manager; + bool have_background_blur; +#endif + +#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED + struct zwp_text_input_manager_v3 *text_input_manager; +#endif + + tll(struct monitor) monitors; /* All available outputs */ + tll(struct seat) seats; + + tll(struct terminal *) terms; + + /* WL_SHM >= 2 */ + bool use_shm_release; + + bool shm_have_argb2101010:1; + bool shm_have_abgr2101010:1; + bool shm_have_abgr161616:1; +}; + +struct wayland *wayl_init( + struct fdm *fdm, struct key_binding_manager *key_binding_manager, + bool presentation_timings); +void wayl_destroy(struct wayland *wayl); + +bool wayl_reload_xcursor_theme(struct seat *seat, float new_scale); + +void wayl_flush(struct wayland *wayl); +void wayl_roundtrip(struct wayland *wayl); + +bool wayl_fractional_scaling(const struct wayland *wayl); +void wayl_surface_scale( + const struct wl_window *win, const struct wayl_surface *surf, + const struct buffer *buf, float scale); +void wayl_surface_scale_explicit_width_height( + const struct wl_window *win, const struct wayl_surface *surf, + int width, int height, float scale); + +struct wl_window *wayl_win_init(struct terminal *term, const char *token); +void wayl_win_destroy(struct wl_window *win); + +void wayl_win_scale(struct wl_window *win, const struct buffer *buf); +void wayl_win_alpha_changed(struct wl_window *win); +bool wayl_win_set_urgent(struct wl_window *win); +bool wayl_win_ring_bell(const struct wl_window *win); + +bool wayl_win_csd_titlebar_visible(const struct wl_window *win); +bool wayl_win_csd_borders_visible(const struct wl_window *win); + +bool wayl_win_subsurface_new( + struct wl_window *win, struct wayl_sub_surface *surf, + bool allow_pointer_input); +bool wayl_win_subsurface_new_with_custom_parent( + struct wl_window *win, struct wl_surface *parent, + struct wayl_sub_surface *surf, bool allow_pointer_input); +void wayl_win_subsurface_destroy(struct wayl_sub_surface *surf); + +bool wayl_get_activation_token( + struct wayland *wayl, struct seat *seat, uint32_t serial, + struct wl_window *win, activation_token_cb_t cb, void *cb_data); +void wayl_activate(struct wayland *wayl, struct wl_window *win, const char *token); + +bool wayl_do_linear_blending(const struct wayland *wayl, const struct config *conf); diff --git a/xkbcommon-vmod.h b/xkbcommon-vmod.h new file mode 100644 index 0000000..44d818e --- /dev/null +++ b/xkbcommon-vmod.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +/* Added in libxkbcommon 1.8.0 */ +#if !defined(XKB_VMOD_NAME_ALT) +/* Common *virtual* modifiers, encoded in xkeyboard-config in the compat and + * symbols files. They have been stable since the beginning of the project and + * are unlikely to ever change. */ +#define XKB_VMOD_NAME_ALT "Alt" +#define XKB_VMOD_NAME_HYPER "Hyper" +#define XKB_VMOD_NAME_LEVEL3 "LevelThree" +#define XKB_VMOD_NAME_LEVEL5 "LevelFive" +#define XKB_VMOD_NAME_META "Meta" +#define XKB_VMOD_NAME_NUM "NumLock" +#define XKB_VMOD_NAME_SCROLL "ScrollLock" +#define XKB_VMOD_NAME_SUPER "Super" +#endif diff --git a/xmalloc.c b/xmalloc.c new file mode 100644 index 0000000..ccfb5c4 --- /dev/null +++ b/xmalloc.c @@ -0,0 +1,96 @@ +#include +#include +#include +#include "xmalloc.h" +#include "debug.h" + +static void * +check_alloc(void *alloc) +{ + if (unlikely(alloc == NULL)) { + FATAL_ERROR(__func__, ENOMEM); + } + return alloc; +} + +void * +xmalloc(size_t size) +{ + if (unlikely(size == 0)) { + size = 1; + } + return check_alloc(malloc(size)); +} + +void * +xcalloc(size_t nmemb, size_t size) +{ + xassert(size != 0); + return check_alloc(calloc(likely(nmemb) ? nmemb : 1, size)); +} + +void * +xrealloc(void *ptr, size_t size) +{ + xassert(size != 0); + void *alloc = realloc(ptr, size); + return check_alloc(alloc); +} + +void * +xreallocarray(void *ptr, size_t n, size_t size) +{ + xassert(n != 0 && size != 0); + void *alloc = reallocarray(ptr, n, size); + return check_alloc(alloc); +} + +char * +xstrdup(const char *str) +{ + return check_alloc(strdup(str)); +} + +char * +xstrndup(const char *str, size_t n) +{ + return check_alloc(strndup(str, n)); +} + +char32_t * +xc32dup(const char32_t *str) +{ + return check_alloc(c32dup(str)); +} + +static VPRINTF(2) int +xvasprintf_(char **strp, const char *format, va_list ap) +{ + va_list ap2; + va_copy(ap2, ap); + int n = vsnprintf(NULL, 0, format, ap2); + if (unlikely(n < 0)) { + FATAL_ERROR("vsnprintf", EILSEQ); + } + va_end(ap2); + *strp = xmalloc(n + 1); + return vsnprintf(*strp, n + 1, format, ap); +} + +char * +xvasprintf(const char *format, va_list ap) +{ + char *str; + xvasprintf_(&str, format, ap); + return str; +} + +char * +xasprintf(const char *format, ...) +{ + va_list ap; + va_start(ap, format); + char *str = xvasprintf(format, ap); + va_end(ap); + return str; +} diff --git a/xmalloc.h b/xmalloc.h new file mode 100644 index 0000000..03e6eb0 --- /dev/null +++ b/xmalloc.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "char32.h" +#include "macros.h" + +void *xmalloc(size_t size) XMALLOC; +void *xcalloc(size_t nmemb, size_t size) XMALLOC; +void *xrealloc(void *ptr, size_t size); +void *xreallocarray(void *ptr, size_t n, size_t size); +char *xstrdup(const char *str) XSTRDUP; +char *xstrndup(const char *str, size_t n) XSTRDUP; +char *xasprintf(const char *format, ...) PRINTF(1) XMALLOC; +char *xvasprintf(const char *format, va_list va) VPRINTF(1) XMALLOC; +char32_t *xc32dup(const char32_t *str) XSTRDUP; + +static inline void * +xmemdup(const void *ptr, size_t size) +{ + return memcpy(xmalloc(size), ptr, size); +} + +static inline char * +xstrjoin(const char *s1, const char *s2) +{ + size_t n1 = strlen(s1); + size_t n2 = strlen(s2); + char *joined = xmalloc(n1 + n2 + 1); + memcpy(joined, s1, n1); + memcpy(joined + n1, s2, n2 + 1); + return joined; +} + +static inline char * +xstrjoin3(const char *s1, const char *s2, const char *s3) +{ + size_t n1 = strlen(s1); + size_t n2 = strlen(s2); + size_t n3 = strlen(s3); + char *joined = xmalloc(n1 + n2 + n3 + 1); + memcpy(joined, s1, n1); + memcpy(joined + n1, s2, n2); + memcpy(joined + n1 + n2, s3, n3 + 1); + return joined; +} diff --git a/xsnprintf.c b/xsnprintf.c new file mode 100644 index 0000000..2f6f849 --- /dev/null +++ b/xsnprintf.c @@ -0,0 +1,55 @@ +#include "xsnprintf.h" + +#include +#include +#include +#include "debug.h" +#include "macros.h" + +/* + * ISO C doesn't require vsnprintf(3) to set errno on failure, but + * POSIX does: + * + * "If an output error was encountered, these functions shall return + * a negative value and set errno to indicate the error." + * + * The mandated errors of interest are: + * + * - EILSEQ: A wide-character code does not correspond to a valid character + * - EOVERFLOW: The value of n is greater than INT_MAX + * - EOVERFLOW: The value to be returned is greater than INT_MAX + * + * ISO C11 states: + * + * "The vsnprintf function returns the number of characters that would + * have been written had n been sufficiently large, not counting the + * terminating null character, or a negative value if an encoding error + * occurred. Thus, the null-terminated output has been completely + * written if and only if the returned value is nonnegative and less + * than n." + * + * See also: + * + * - ISO C11 §7.21.6.12p3 + * - https://pubs.opengroup.org/onlinepubs/9699919799/functions/vsnprintf.html + * - https://pubs.opengroup.org/onlinepubs/9699919799/functions/snprintf.html + */ +static size_t +xvsnprintf(char *restrict buf, size_t n, const char *restrict format, va_list ap) +{ + int len = vsnprintf(buf, n, format, ap); + if (unlikely(len < 0 || len >= (int)n)) { + FATAL_ERROR(__func__, (len < 0) ? errno : ENOBUFS); + } + return (size_t)len; +} + +size_t +xsnprintf(char *restrict buf, size_t n, const char *restrict format, ...) +{ + va_list ap; + va_start(ap, format); + size_t len = xvsnprintf(buf, n, format, ap); + va_end(ap); + return len; +} diff --git a/xsnprintf.h b/xsnprintf.h new file mode 100644 index 0000000..9463a34 --- /dev/null +++ b/xsnprintf.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include +#include "macros.h" + +size_t xsnprintf(char *restrict buf, size_t n, const char *restrict format, ...) PRINTF(3) NONNULL_ARGS;