diff -Nru foot-1.16.2/.woodpecker.yaml foot-1.17.2/.woodpecker.yaml --- foot-1.16.2/.woodpecker.yaml 1970-01-01 00:00:00.000000000 +0000 +++ foot-1.17.2/.woodpecker.yaml 2024-04-17 09:26:45.000000000 +0000 @@ -0,0 +1,134 @@ +# -*- yaml -*- + +steps: + - name: codespell + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + image: alpine:edge + commands: + - apk add openssl + - apk add python3 + - apk add py3-pip + - python3 -m venv codespell-venv + - source codespell-venv/bin/activate + - pip install codespell + - codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd + - deactivate + + - name: subprojects + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + image: alpine:edge + commands: + - apk add git + - mkdir -p subprojects && cd subprojects + - git clone https://codeberg.org/dnkl/tllist.git + - git clone https://codeberg.org/dnkl/fcft.git + - cd .. + + - name: x64 + when: + - event: [manual, pull_request] + - event: [push, tag] + branch: [master, releases/*] + depends_on: [subprojects] + image: alpine:edge + commands: + - apk update + - apk add musl-dev linux-headers meson ninja gcc clang scdoc ncurses + - apk add libxkbcommon-dev pixman-dev freetype-dev fontconfig-dev harfbuzz-dev utf8proc-dev + - apk add wayland-dev wayland-protocols + - apk add git + - apk add check-dev + - apk add ttf-hack font-noto-emoji + + # Debug + - mkdir -p bld/debug-x64 + - cd bld/debug-x64 + - meson --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 --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 --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 --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 --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 --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 --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 -Nru foot-1.16.2/.woodpecker.yml foot-1.17.2/.woodpecker.yml --- foot-1.16.2/.woodpecker.yml 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/.woodpecker.yml 1970-01-01 00:00:00.000000000 +0000 @@ -1,131 +0,0 @@ -steps: - codespell: - when: - branch: - - master - - releases/* - image: alpine:edge - commands: - - apk add python3 - - apk add py3-pip - - python3 -m venv codespell-venv - - source codespell-venv/bin/activate - - pip install codespell - - codespell -Lser,doas,zar README.md INSTALL.md CHANGELOG.md *.c *.h doc/*.scd - - deactivate - - subprojects: - when: - 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 .. - - x64: - when: - branch: - - master - - releases/* - group: build - 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 --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 --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 --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 --buildtype=debug -Dgrapheme-clustering=disabled -Dfcft:grapheme-shaping=disabled -Dfcft:run-shaping=disabled -Dfcft:test-text-shaping=false ../.. - - ninja -v -k0 - - ninja -v test - - ./foot --version - - ./footclient --version - - cd ../.. - - x86: - when: - branch: - - master - - releases/* - group: build - 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 --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 --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 --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 -Nru foot-1.16.2/CHANGELOG.md foot-1.17.2/CHANGELOG.md --- foot-1.16.2/CHANGELOG.md 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/CHANGELOG.md 2024-04-17 09:26:45.000000000 +0000 @@ -1,5 +1,8 @@ # Changelog +* [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) @@ -47,6 +50,204 @@ * [1.2.1](#1-2-1) * [1.2.0](#1-2-0) + +## 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 @@ -317,7 +518,7 @@ * 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”_. + 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 @@ -325,9 +526,9 @@ * 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 +* 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** + 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 @@ -423,8 +624,8 @@ ([#1188][1188]). * Bracketed paste terminfo entries (`BD`, `BE`, `PE` and `PS`, added to ncurses in 2022-12-24). Vim makes use of these. -* “Report version” terminfo entries (`XR`/`xr`). -* “Report DA2” terminfo entries (`RV`/`rv`). +* "Report version" terminfo entries (`XR`/`xr`). +* "Report DA2" terminfo entries (`RV`/`rv`). * `XF` terminfo capability (focus in/out events available). * `$TERM_PROGRAM` and `$TERM_PROGRAM_VERSION` environment variables unset in the slave process. @@ -477,12 +678,12 @@ * Crash when interactively resizing the window with a very large scrollback. * Crash when a sixel image exceeds the current sixel max height. -* Crash after reverse-scrolling (`CSI Ps T`) in the ‘normal’ +* Crash after reverse-scrolling (`CSI Ps T`) in the 'normal' (non-alternate) screen ([#1190][1190]). * Background transparency being applied to the text "behind" the cursor. Only applies to block cursor using inversed fg/bg colors. ([#1205][1205]). -* Crash when monitor’s physical size is "too small" ([#1209][1209]). +* Crash when monitor's physical size is "too small" ([#1209][1209]). * Line-height adjustment when incrementing/decrementing the font size with a user-set line-height ([#1218][1218]). * Scaling factor not being correctly applied when converting pt-or-px @@ -553,7 +754,7 @@ * Crash on buggy compositors (GNOME) that sometimes send pointer-enter events with a NULL surface. Foot now ignores these events, and the subsequent motion and leave events. -* Regression: “random” selected empty cells being highlighted as +* Regression: "random" selected empty cells being highlighted as selected when they should not. * Crash when either resizing the terminal window, or scrolling in the scrollback history ([#1074][1074]) @@ -593,7 +794,7 @@ ### Changed -* Use `$HOME` instead of `getpwuid()` to retrieve the user’s home +* Use `$HOME` instead of `getpwuid()` to retrieve the user's home directory when searching for `foot.ini`. * HT, VT and FF are no longer stripped when pasting in non-bracketed mode ([#1084][1084]). @@ -657,7 +858,7 @@ ### Added * Workaround for Sway bug [#6960][sway-6960]: scrollback search and - the OSC-555 (“flash”) escape sequence leaves dimmed (search) and + the OSC-555 ("flash") escape sequence leaves dimmed (search) and yellow (flash) artifacts ([#1046][1046]). * `Control+Shift+v` and `XF86Paste` have been added to the default set of key bindings that paste from the clipboard into the scrollback @@ -670,7 +871,7 @@ ### Changed -* Scrollback search’s `extend-to-word-boundary` no longer stops at +* Scrollback search's `extend-to-word-boundary` no longer stops at space-to-word boundaries, making selection extension feel more natural. @@ -711,7 +912,7 @@ ([#950][950]). * footclient: `-E,--client-environment` command line option. When used, the child process in the new terminal instance inherits the - environment from the footclient process instead of the server’s + environment from the footclient process instead of the server's ([#1004][1004]). * `[csd].hide-when-maximized=yes|no` option ([#1019][1019]). * Scrollback search mode now highlights all matches. @@ -767,7 +968,7 @@ * Build: missing `wayland_client` dependency in `test-config` ([#918][918]). -* “(null)” being logged as font-name (for some fonts) when warning +* "(null)" being logged as font-name (for some fonts) when warning about a non-monospaced primary font. * Rare crash when the window is resized while a mouse selection is ongoing ([#922][922]). @@ -789,7 +990,7 @@ ([#1009][1009]). * Window geometry when CSDs are enabled and CSD border width set to a non-zero value. This fixes window snapping in e.g. GNOME. -* Window size “jumping” when starting an interactive resize when CSDs +* Window size "jumping" when starting an interactive resize when CSDs are enabled, and CSD border width set to a non-zero value. * Key binding overrides on the command line having no effect with `footclient` instances ([#931][931]). @@ -855,15 +1056,15 @@ * PaperColorDark and PaperColorLight themes renamed to paper-color-dark and paper-color-light, for consistency with other theme names. -* `[scrollback].multiplier` is now applied in “alternate scroll” mode, +* `[scrollback].multiplier` is now applied in "alternate scroll" mode, where scroll events are translated to fake arrow key presses on the alt screen ([#859](https://codeberg.org/dnkl/foot/issues/859)). -* The width of the block cursor’s outline in an unfocused window is - now scaled by the output scaling factor (“desktop - scaling”). Previously, it was always 1px. -* Foot will now try to change the locale to either “C.UTF-8” or - “en_US.UTF-8” if started with a non-UTF8 locale. If this fails, foot - will start, but only to display a window with an error (user’s shell +* The width of the block cursor's outline in an unfocused window is + now scaled by the output scaling factor ("desktop + scaling"). Previously, it was always 1px. +* Foot will now try to change the locale to either "C.UTF-8" or + "en_US.UTF-8" if started with a non-UTF8 locale. If this fails, foot + will start, but only to display a window with an error (user's shell is not executed). * `gettimeofday()` has been replaced with `clock_gettime()`, due to it being marked as obsolete by POSIX. @@ -891,7 +1092,7 @@ ### Fixed -* Font size adjustment (“zooming”) when font is configured with a +* Font size adjustment ("zooming") when font is configured with a **pixelsize**, and `dpi-aware=no` ([#842](https://codeberg.org/dnkl/foot/issues/842)). * Key presses triggering keyboard layout switches also emitting CSI @@ -965,7 +1166,7 @@ * Initial support for the [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/). Modes supported: - [Disambiguate escape codes](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#disambiguate) (mode `0b1`) -* “Window menu” (compositor provided) on right clicks on the CSD title +* "Window menu" (compositor provided) on right clicks on the CSD title bar. @@ -1002,7 +1203,7 @@ ### Fixed -* Regression: `letter-spacing` resulting in a “not a valid option” +* Regression: `letter-spacing` resulting in a "not a valid option" error ([#795](https://codeberg.org/dnkl/foot/issues/795)). * Regression: bad section name in configuration error messages. * Regression: `pipe-*` key bindings not being parsed correctly, @@ -1037,7 +1238,7 @@ * `[csd].border-width` and `[csd].border-color`, allowing you to configure the width and color of the CSD border. * Support for `XTMODKEYS` with `Pp=4` and `Pv=2` (_modifyOtherKeys=2_). -* `[colors].dim0-7` options, allowing you to configure custom “dim” +* `[colors].dim0-7` options, allowing you to configure custom "dim" colors ([#776](https://codeberg.org/dnkl/foot/issues/776)). @@ -1053,9 +1254,9 @@ due to the compositor not implementing a recent enough version of the `wl_seat` interface ([#779](https://codeberg.org/dnkl/foot/issues/779)). * Boolean options in `foot.ini` are now limited to - “yes|true|on|1|no|false|off|0”, Previously, anything that did not - match “yes|true|on”, or a number greater than 0, was treated as - “false”. + "yes|true|on|1|no|false|off|0", Previously, anything that did not + match "yes|true|on", or a number greater than 0, was treated as + "false". * `[scrollback].multiplier` is no longer applied when the alternate screen is in use ([#787](https://codeberg.org/dnkl/foot/issues/787)). @@ -1070,7 +1271,7 @@ ### Fixed -* ‘Sticky’ modifiers in input handling; when determining modifier +* 'Sticky' modifiers in input handling; when determining modifier state, foot was looking at **depressed** modifiers, not **effective** modifiers, like it should. * Fix crashes after enabling CSD at runtime when `csd.size` is 0. @@ -1078,7 +1279,7 @@ ([#752](https://codeberg.org/dnkl/foot/issues/752)). * Clipboard occasionally ceasing to work, until window has been re-focused ([#753](https://codeberg.org/dnkl/foot/issues/753)). -* Don’t propagate window title updates to the Wayland compositor +* Don't propagate window title updates to the Wayland compositor unless the new title is different from the old title. @@ -1098,7 +1299,7 @@ ### Changed * PGO helper scripts no longer set `LC_CTYPE=en_US.UTF-8`. But, note - that “full” PGO builds still **require** a UTF-8 locale; you need + that "full" PGO builds still **require** a UTF-8 locale; you need to set one manually in your build script ([#728](https://codeberg.org/dnkl/foot/issues/728)). @@ -1126,11 +1327,11 @@ definitions when `-Dterminfo=enabled`. * `-Dcustom-terminfo-install-location` no longer accepts `no` as a special value, to disable exporting `TERMINFO`. To achieve the same - result, simply don’t set it at all. If it _is_ set, `TERMINFO` is + result, simply don't set it at all. If it _is_ set, `TERMINFO` is still exported, like before. * The default install location for the terminfo definitions have been changed back to `${datadir}/terminfo`. -* `dpi-aware=auto`: fonts are now scaled using the monitor’s DPI only +* `dpi-aware=auto`: fonts are now scaled using the monitor's DPI only when **all** monitors have a scaling factor of one ([#714](https://codeberg.org/dnkl/foot/issues/714)). * fcft >= 3.0.0 in now required. @@ -1179,12 +1380,12 @@ terminating the client application) from 4 to 60 seconds. * When terminating the client application, foot now sends `SIGTERM` immediately after closing the PTY, instead of waiting 2 seconds. -* Foot now sends `SIGTERM`/`SIGKILL` to the client application’s process group, - instead of just to the client application’s process. +* Foot now sends `SIGTERM`/`SIGKILL` to the client application's process group, + instead of just to the client application's process. * `kmous` terminfo capability from `\E[M` to `\E[<`. * pt-or-px values (`letter-spacing`, etc) and the line thickness (`tweak.box-drawing-base-thickness`) in box drawing characters are - now translated to pixel values using the monitor’s scaling factor + now translated to pixel values using the monitor's scaling factor when `dpi-aware=no`, or `dpi-aware=auto` and the scaling factor is larger than 1 ([#680](https://codeberg.org/dnkl/foot/issues/680)). * Spawning a new terminal with a working directory that does not exist @@ -1194,7 +1395,7 @@ ### Removed * `km`/`smm`/`rmm` from terminfo; foot prefixes Alt-key combinations - with `ESC`, and not by setting the 8:th “meta” bit, regardless of + with `ESC`, and not by setting the 8:th "meta" bit, regardless of `smm`/`rmm`. While this _can_ be disabled by, resetting private mode 1036, the terminfo should reflect the **default** behavior ([#670](https://codeberg.org/dnkl/foot/issues/670)). @@ -1361,10 +1562,10 @@ If `tweak.grapheme-shaping` has **not** been enabled, foot will neither use libutf8proc to do grapheme cluster segmentation, nor will -it use fcft’s grapheme shaping capabilities to shape combining +it use fcft's grapheme shaping capabilities to shape combining characters. -This feature is _experimental_ mostly due to the “wcwidth” problem; +This feature is _experimental_ mostly due to the "wcwidth" problem; how many cells should foot allocate for a grapheme cluster? While the answer may seem simple, the problem is that, whatever the answer is, the client application **must** come up with the **same** @@ -1442,9 +1643,9 @@ * Point values in `line-height`, `letter-spacing`, `horizontal-letter-offset` and `vertical-letter-offset` are now rounded, not truncated, when translated to pixel values. -* Foot’s exit code is now -26/230 when foot itself failed to launch +* Foot's exit code is now -26/230 when foot itself failed to launch (due to invalid command line options, client application/shell not - found etc). Footclient’s exit code is -36/220 when it itself fails + found etc). Footclient's exit code is -36/220 when it itself fails to launch (e.g. bad command line option) and -26/230 when the foot server failed to instantiate a new window ([#466](https://codeberg.org/dnkl/foot/issues/466)). @@ -1497,7 +1698,7 @@ resulting in PGO build failures. * Wrong colors in the 256-color cube ([#479](https://codeberg.org/dnkl/foot/issues/479)). -* Memory leak triggered by “opening” an OSC-8 URI and then resetting +* Memory leak triggered by "opening" an OSC-8 URI and then resetting the terminal without closing the URI ([#495](https://codeberg.org/dnkl/foot/issues/495)). * Assertion when emitting a sixel occupying the entire scrollback @@ -1506,7 +1707,7 @@ invisible) for certain combinations of fonts and font sizes ([#503](https://codeberg.org/dnkl/foot/issues/503)). * Sixels with transparent bottom border being resized below the size - specified in _”Set Raster Attributes”_. + specified in _"Set Raster Attributes"_. * Fonts sometimes not being reloaded with the correct scaling factor when `dpi-aware=no`, or `dpi-aware=auto` with monitor(s) with a scaling factor > 1 ([#509](https://codeberg.org/dnkl/foot/issues/509)). @@ -1702,7 +1903,7 @@ background color for empty pixels instead of the default background color ([#391](https://codeberg.org/dnkl/foot/issues/391)). * Sixel decoding optimized; up to 100% faster in some cases. -* Reported sixel “max geometry” from current window size, to the +* Reported sixel "max geometry" from current window size, to the configured maximum size (defaulting to 10000x10000). @@ -1801,7 +2002,7 @@ * Pasting URIs from the clipboard when the source has not newline-terminated the last URI ([#291](https://codeberg.org/dnkl/foot/issues/291)). -* Sixel “current geometry” query response not being bounded by the +* Sixel "current geometry" query response not being bounded by the current window dimensions (fixes `lsix` output) * Crash on keyboard input when repeat rate was zero (i.e. no repeat). * Wrong button encoding of mouse buttons 6 and 7 in mouse events. @@ -1878,7 +2079,7 @@ and `CSI ? 737769 l` disables it. This can be used to e.g. enable/disable IME when entering/leaving insert mode in vim. * `dpi-aware` option to `foot.ini`. The default, `auto`, sizes fonts - using the monitor’s DPI when output scaling has been + using the monitor's DPI when output scaling has been **disabled**. If output scaling has been **enabled**, fonts are sized using the scaling factor. DPI-only font sizing can be forced by setting `dpi-aware=yes`. Setting `dpi-aware=no` forces font @@ -1958,7 +2159,7 @@ `\E[38:2...m`) can now be used _without_ the color space ID parameter. * SGR 21 no longer disables **bold**. According to ECMA-48, SGR 21 is - _”double underline_”. Foot does not (yet) implement that, but that’s + _"double underline_". Foot does not (yet) implement that, but that's no reason to implement a non-standard behavior. * `DECRQM` now returns actual state of the requested mode, instead of always returning `2`. @@ -2006,7 +2207,7 @@ ([#194](https://codeberg.org/dnkl/foot/issues/194)). * Single-width characters with double-width glyphs are now allowed to overflow into neighboring cells by default. Set - **tweak.allow-overflowing-double-width-glyphs** to ‘no’ to disable + **tweak.allow-overflowing-double-width-glyphs** to 'no' to disable this. ### Fixed @@ -2210,7 +2411,7 @@ binding has consumed it. * Input events from getting mixed with paste data ([#101](https://codeberg.org/dnkl/foot/issues/101)). -* Missing DPI values for “some” monitors on Gnome +* Missing DPI values for "some" monitors on Gnome ([#118](https://codeberg.org/dnkl/foot/issues/118)). * Handling of multi-column composed characters while reflowing. * Escape sequences sent for key combinations with `Return`, that did diff -Nru foot-1.16.2/CODE_OF_CONDUCT.md foot-1.17.2/CODE_OF_CONDUCT.md --- foot-1.16.2/CODE_OF_CONDUCT.md 1970-01-01 00:00:00.000000000 +0000 +++ foot-1.17.2/CODE_OF_CONDUCT.md 2024-04-17 09:26:45.000000000 +0000 @@ -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 truely correct a past +pattern of unacceptable behavior. That is to say, the scope of the Code of +Conduct does not necessarily extend into the distant past. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior +may be reported to the community leaders responsible for enforcement +at [daniel@ekloef.se](mailto:daniel@ekloef.se). All complaints will +be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +The consequences for Code of Conduct violations will be decided upon and +enforced by community leaders. These may include a formal warning, a temporary +ban from community spaces, a permanent ban from community spaces, etc. + +There are no hard and fast rules for exactly what behavior in which space will +result in what consequences, it is up to the community leaders to enforce the +Code of Conduct in a way that they believe best promotes a healthy community. + +## Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant](https://www.contributor-covenant.org/), +version 2.1, available at +https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. diff -Nru foot-1.16.2/INSTALL.md foot-1.17.2/INSTALL.md --- foot-1.16.2/INSTALL.md 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/INSTALL.md 2024-04-17 09:26:45.000000000 +0000 @@ -94,24 +94,24 @@ capabilities to the applications running inside the terminal. As such, it is important that the terminfo used reflects the actual terminal. Using the `xterm-256color` terminfo will, in many cases, -work, but I still recommend using foot’s own terminfo. There are two +work, but I still recommend using foot's own terminfo. There are two reasons for this: -* foot’s terminfo contains a couple of non-standard capabilities, +* foot's terminfo contains a couple of non-standard capabilities, used by e.g. tmux. * New capabilities added to the `xterm-256color` terminfo could potentially break foot. -* There may be future additions or changes to foot’s terminfo. +* There may be future additions or changes to foot's terminfo. -As of ncurses 2021-07-31, ncurses includes a version of foot’s +As of ncurses 2021-07-31, ncurses includes a version of foot's terminfo. **The recommendation is to use those**, and only install the -terminfo definitions from this git repo if the system’s ncurses +terminfo definitions from this git repo if the system's ncurses predates 2021-07-31. -But, note that the foot terminfo definitions in ncurses’ lack the +But, note that the foot terminfo definitions in ncurses' lack the non-standard capabilities. This mostly affects tmux; without them, `terminal-overrides` must be configured to enable truecolor -support. For this reason, it _is_ possible to install “our” terminfo +support. For this reason, it _is_ possible to install "our" terminfo definitions as well, either in a non-default location, or under a different name. @@ -124,10 +124,10 @@ Installing them under a different name generally works well, but will break applications that check if `$TERM == foot`. -Hence the recommendation to simply use ncurses’ terminfo definitions +Hence the recommendation to simply use ncurses' terminfo definitions if available. -If packaging “our” terminfo definitions, I recommend doing that as a +If packaging "our" terminfo definitions, I recommend doing that as a separate package, to allow them to be installed on remote systems without having to install foot itself. @@ -176,9 +176,9 @@ ``` (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 +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 @@ -194,7 +194,7 @@ `TERM`, and it does **not** disable `TERMINFO`, if `-Dcustom-terminfo-install-location` has been set. Use this if packaging the terminfo definitions in a separate package (and the -build script isn’t shared with the ‘foot’ package). +build script isn't shared with the 'foot' package). Example: @@ -269,7 +269,7 @@ scripts in the `pgo` directory to do a complete PGO build. This script is intended to be used when doing manual builds. -Note that all “full” PGO builds (which `auto` will prefer, if +Note that all "full" PGO builds (which `auto` will prefer, if possible) **require** `LC_CTYPE` to be set to an UTF-8 locale. This is **not** done automatically. @@ -370,7 +370,7 @@ The snippet above then creates an (empty) temporary file. Then, it runs a script that generates random escape sequences (if you cat -`${tmp_file}` in a terminal, you’ll see random colored characters all +`${tmp_file}` in a terminal, you'll see random colored characters all over the screen). Finally, we feed the randomly generated escape sequences to the PGO helper. This is what generates the profiling data used in the next step. @@ -450,7 +450,7 @@ tic -o -x -e foot,foot-direct - ``` -Where _”output-directory”_ **must** match the value passed to +Where _"output-directory"_ **must** match the value passed to `-Dcustom-terminfo-install-location` in the foot build. If `-Dcustom-terminfo-install-location` has not been set, `-o ` can simply be omitted. diff -Nru foot-1.16.2/README.md foot-1.17.2/README.md --- foot-1.16.2/README.md 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/README.md 2024-04-17 09:26:45.000000000 +0000 @@ -28,6 +28,7 @@ 1. [Shell integration](#shell-integration) 1. [Current working directory](#current-working-directory) 1. [Jumping between prompts](#jumping-between-prompts) + 1. [Piping last command's output](#piping-last-command-s-output) 1. [Alt/meta](#alt-meta) 1. [Backspace](#backspace) 1. [Keypad](#keypad) @@ -36,6 +37,7 @@ 1. [Programmatically checking if running in foot](#programmatically-checking-if-running-in-foot) 1. [XTGETTCAP](#xtgettcap) 1. [Credits](#Credits) +1. [Code of Conduct](#code-of-conduct) 1. [Bugs](#bugs) 1. [Contact](#contact) 1. [IRC](#irc) @@ -150,10 +152,10 @@ : Start a scrollback search ctrl++, ctrl+= -: Increase font size by 0,5pt +: Increase font size ctrl+- -: Decrease font size by 0,5pt +: Decrease font size ctrl+0 : Reset font size @@ -236,7 +238,11 @@ under the pointer up to, and until, the next space characters. left - **triple-click** -: Selects the entire row +: Selects the everything between enclosing quotes, or the entire row + if not inside a quote. + +left - **quad-click** +: Selects the entire row. middle : Paste from _primary_ selection @@ -246,9 +252,16 @@ 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 @@ -292,7 +305,7 @@ desktop), and then run `footclient` instead of `foot` whenever you want to launch a new terminal. -Foot support socket activation, which means `foot --server` will only be +Foot supports socket activation, which means `foot --server` will only be started the first time you'll run `footclient`. (systemd user units are included, but it can work with other supervision suites). @@ -302,10 +315,10 @@ emulators, where URLs are highlighted when they are hovered and opened by clicking on them, foot uses a keyboard driven approach. -Pressing ctrl+shift+o enters _“URL -mode”_, where all currently visible URLs are underlined, and is -associated with a _“jump-label”_. The jump-label indicates the _key -sequence_ (e.g. **”AF”**) to use to activate the URL. +Pressing ctrl+shift+o enters _"URL +mode"_, where all currently visible URLs are underlined, and is +associated with a _"jump-label"_. The jump-label indicates the _key +sequence_ (e.g. **"AF"**) to use to activate the URL. The key binding can, of course, be customized, like all other key bindings in foot. See `show-urls-launch` and `show-urls-copy` in the @@ -328,7 +341,7 @@ New foot terminal instances (bound to ctrl+shift+n by default) will open in -the current working directory, **if** the shell in the “parent” +the current working directory, **if** the shell in the "parent" terminal reports directory changes. This is done with the OSC-7 escape sequence. Most shells can be @@ -359,6 +372,42 @@ [wiki](https://codeberg.org/dnkl/foot/wiki#user-content-jumping-between-prompts) for details, and examples for other shells. +### Piping last command's output + +The key binding `pipe-command-output` can pipe the last command's +output to an application of your choice (similar to the other `pipe-*` +key bindings): + +```ini +[key-bindings] +pipe-command-output=[sh -c "f=$(mktemp); cat - > $f; footclient emacsclient -nw $f; rm $f"] Control+Shift+g +``` + +When pressing ctrl+shift+g, the last +command's output is written to a temporary file, then an emacsclient +is started in a new footclient instance. The temporary file is removed +after the footclient instance has closed. + +For this to work, the shell must emit an OSC-133;C (`\E]133;C\E\\`) +sequence before command output starts, and an OSC-133;D +(`\E]133;D\E\\`) when the command output ends. + +In fish, one way to do this is to add `preexec` and `postexec` hooks: + +```fish +function foot_cmd_start --on-event fish_preexec + echo -en "\e]133;C\e\\" +end + +function foot_cmd_end --on-event fish_postexec + echo -en "\e]133;D\e\\" +end +``` + +See the +[wiki](https://codeberg.org/dnkl/foot/wiki#user-content-piping-last-command-s-output) +for details, and examples for other shells + ## Alt/meta @@ -380,13 +429,13 @@ ## Backspace Foot transmits DEL (`^?`) on backspace. This corresponds to -XTerm's `backarrowKey` option set to `false`, and to DECBKM being -_reset_. +XTerm's `backarrowKey` option set to `false`, and to +[`DECBKM`](https://vt100.net/docs/vt510-rm/DECBKM.html) being _reset_. To instead transmit BS (`^H`), press ctrl+backspace. -Note that foot does **not** implement DECBKM, and that the behavior +Note that foot does **not** implement `DECBKM`, and that the behavior described above **cannot** be changed. Finally, pressing alt will prefix the transmitted byte with @@ -435,19 +484,19 @@ For this reason, and because of the new _fractional scaling_ protocol (see below for details), and because this is how Wayland applications are expected to behave, foot >= 1.15 will default to scaling fonts -using the compositor’s scaling factor, and **not** the monitor +using the compositor's scaling factor, and **not** the monitor DPI. This means the (assuming the monitors are at the same viewing distance) the font size will appear to change when you move the foot window across different monitors, **unless** you have configured the -monitors’ scaling factors correctly in the compositor. +monitors' scaling factors correctly in the compositor. This can be changed by setting the `dpi-aware` option to `yes` in `foot.ini`. When enabled, fonts will **not** be sized using the -scaling factor, but will instead be sized using the monitor’s +scaling factor, but will instead be sized using the monitor's DPI. When the foot window is moved across monitors, the font size is -updated for the current monitor’s DPI. +updated for the current monitor's DPI. This means that, assuming the monitors are **at the same viewing distance**, the font size will appear to be the same, at all times. @@ -499,6 +548,7 @@ * `OSC 117` - reset highlight background color * `OSC 119` - reset highlight foreground color * `OSC 133` - [shell integration](#shell-integration) +* `OSC 176` - set app ID * `OSC 555` - flash screen (**foot specific**) * `OSC 777` - desktop notification (only the `;notify` sub-command of OSC 777 is supported.) @@ -537,7 +587,7 @@ Starting with version 1.7.0, foot also implements `XTVERSION`, to which it will reply with `\EP>|foot(version)\E\\`. Version is -e.g. “1.8.2” for a regular release, or “1.8.2-36-g7db8e06f” for a git +e.g. "1.8.2" for a regular release, or "1.8.2-36-g7db8e06f" for a git build. @@ -550,9 +600,9 @@ capabilities. Applications using this feature do not need to use the classic, file-based, terminfo definition. For example, if all applications used this feature, you would no longer have to install -foot’s terminfo on remote hosts you SSH into. +foot's terminfo on remote hosts you SSH into. -XTerm’s implementation (as of XTerm-370) only supports querying key +XTerm's implementation (as of XTerm-370) only supports querying key (as in keyboard keys) capabilities, and three custom capabilities: * `TN` - terminal name @@ -564,7 +614,7 @@ string capabilities. Foot supports this, and extends it even further, to also include -boolean capabilities. This means foot’s entire terminfo can be queried +boolean capabilities. This means foot's entire terminfo can be queried via `XTGETTCAP`. Note that both Kitty and foot handles **responses** to @@ -576,7 +626,7 @@ * The success/fail flag in the beginning of the response is always `1` (success), unless the very **first** queried capability is invalid. * XTerm will not respond **at all** to an invalid capability, unless - it’s the first one in the `XTGETTCAP` query. + it's the first one in the `XTGETTCAP` query. * XTerm will end the response at the first invalid capability. In other words, if you send a large multi-capability query, you will @@ -595,6 +645,11 @@ contributing foot's [logo](icons/hicolor/48x48/apps/foot.png). +# Code of Conduct + +See [Code of Conduct](CODE_OF_CONDUCT.md) + + # Bugs Please report bugs to https://codeberg.org/dnkl/foot/issues diff -Nru foot-1.16.2/base64.c foot-1.17.2/base64.c --- foot-1.16.2/base64.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/base64.c 2024-04-17 09:26:45.000000000 +0000 @@ -36,12 +36,9 @@ }; static const char lookup[64] = { - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', - 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', - 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - '+', '/', + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/" }; char * diff -Nru foot-1.16.2/box-drawing.c foot-1.17.2/box-drawing.c --- foot-1.16.2/box-drawing.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/box-drawing.c 2024-04-17 09:26:45.000000000 +0000 @@ -2098,7 +2098,7 @@ if (x_px_left >= 1) { x_spacing++; x_px_left--; } if (y_px_left >= 3) { y_spacing++; y_px_left -= 3; } - /* Fourth, margins (“spacing”, but on the sides) */ + /* Fourth, margins ("spacing", but on the sides) */ if (x_px_left >= 2) { x_margin++; x_px_left -= 2; } if (y_px_left >= 2) { y_margin++; y_px_left -= 2; } diff -Nru foot-1.16.2/char32.c foot-1.17.2/char32.c --- foot-1.16.2/char32.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/char32.c 2024-04-17 09:26:45.000000000 +0000 @@ -129,11 +129,11 @@ UNITTEST { - char32_t *c = c32dup(U"foobar"); + char32_t *c = xc32dup(U"foobar"); xassert(c32cmp(c, U"foobar") == 0); free(c); - c = c32dup(U""); + c = xc32dup(U""); xassert(c32cmp(c, U"") == 0); free(c); } diff -Nru foot-1.16.2/client.c foot-1.17.2/client.c --- foot-1.16.2/client.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/client.c 2024-04-17 09:26:45.000000000 +0000 @@ -315,11 +315,11 @@ } case 'l': - if (optarg == NULL || strcmp(optarg, "auto") == 0) + if (optarg == NULL || streq(optarg, "auto")) log_colorize = LOG_COLORIZE_AUTO; - else if (strcmp(optarg, "never") == 0) + else if (streq(optarg, "never")) log_colorize = LOG_COLORIZE_NEVER; - else if (strcmp(optarg, "always") == 0) + else if (streq(optarg, "always")) log_colorize = LOG_COLORIZE_ALWAYS; else { fprintf(stderr, "%s: argument must be one of 'never', 'always' or 'auto'\n", optarg); @@ -419,7 +419,7 @@ if (resolved_path_cwd != NULL && resolved_path_pwd != NULL && - strcmp(resolved_path_cwd, resolved_path_pwd) == 0) + streq(resolved_path_cwd, resolved_path_pwd)) { /* * The resolved path of $PWD matches the resolved path of diff -Nru foot-1.16.2/commands.c foot-1.17.2/commands.c --- foot-1.16.2/commands.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/commands.c 2024-04-17 09:26:45.000000000 +0000 @@ -23,7 +23,7 @@ const int grid_rows = grid->num_rows; /* The view row number in scrollback relative coordinates. This is - * the maximum number of rows we’re allowed to scroll */ + * the maximum number of rows we're allowed to scroll */ int sb_start = grid_sb_start_ignore_uninitialized(grid, term->rows); int view_sb_rel = grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, view); diff -Nru foot-1.16.2/completions/bash/foot foot-1.17.2/completions/bash/foot --- foot-1.16.2/completions/bash/foot 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/completions/bash/foot 2024-04-17 09:26:45.000000000 +0000 @@ -19,6 +19,7 @@ "--maximized" "--override" "--print-pid" + "--pty" "--server" "--term" "--title" @@ -39,7 +40,7 @@ for word in "${previous_words[@]}" ; do match=$(printf "$commands" | grep -Fx "$word" 2>/dev/null) if [[ ! -z "$match" ]] ; then - if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--config|--font|--log-level|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then + if [[ ${COMP_WORDS[i-1]} =~ ^(--app-id|--config|--font|--log-level|--pty|--term|--title|--window-size-pixels|--window-size-chars|--working-directory)$ ]] ; then (( i++ )) continue fi @@ -74,7 +75,7 @@ COMPREPLY=( $(compgen -W "none error warning info" -- ${cur}) ) ;; --log-colorize|-l) COMPREPLY=( $(compgen -W "never always auto" -- ${cur}) ) ;; - --app-id|--help|--override|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC]) + --app-id|--help|--override|--pty|--title|--version|--window-size-chars|--window-size-pixels|--check-config|-[ahoTvWwC]) # Don't autocomplete for these flags : ;; *) diff -Nru foot-1.16.2/completions/fish/foot.fish foot-1.17.2/completions/fish/foot.fish --- foot-1.16.2/completions/fish/foot.fish 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/completions/fish/foot.fish 2024-04-17 09:26:45.000000000 +0000 @@ -18,5 +18,6 @@ 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 -Nru foot-1.16.2/completions/zsh/_foot foot-1.17.2/completions/zsh/_foot --- foot-1.16.2/completions/zsh/_foot 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/completions/zsh/_foot 2024-04-17 09:26:45.000000000 +0000 @@ -18,6 +18,7 @@ '(-s --server)'{-s,--server}'[run as server; open terminals by running footclient]:server:_files' \ '(-H --hold)'{-H,--hold}'[remain open after child process exits]' \ '(-p --print-pid)'{-p,--print-pid}'[print PID to this file or FD when up and running (server mode only)]:pidfile:_files' \ + '--pty=[display an existing pty instead of creating one]:pty:_files' \ '(-d --log-level)'{-d,--log-level}'[log level (warning)]:loglevel:(info warning error none)' \ '(-l --log-colorize)'{-l,--log-colorize}'[enable or disable colorization of log output on stderr]:logcolor:(never always auto)' \ '(-S --log-no-syslog)'{-s,--log-no-syslog}'[disable syslog logging (server mode only)]' \ diff -Nru foot-1.16.2/config.c foot-1.17.2/config.c --- foot-1.16.2/config.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/config.c 2024-04-17 09:26:45.000000000 +0000 @@ -111,6 +111,7 @@ [BIND_ACTION_PIPE_SCROLLBACK] = "pipe-scrollback", [BIND_ACTION_PIPE_VIEW] = "pipe-visible", [BIND_ACTION_PIPE_SELECTED] = "pipe-selected", + [BIND_ACTION_PIPE_COMMAND_OUTPUT] = "pipe-command-output", [BIND_ACTION_SHOW_URLS_COPY] = "show-urls-copy", [BIND_ACTION_SHOW_URLS_LAUNCH] = "show-urls-launch", [BIND_ACTION_SHOW_URLS_PERSISTENT] = "show-urls-persistent", @@ -118,6 +119,7 @@ [BIND_ACTION_PROMPT_PREV] = "prompt-prev", [BIND_ACTION_PROMPT_NEXT] = "prompt-next", [BIND_ACTION_UNICODE_INPUT] = "unicode-input", + [BIND_ACTION_QUIT] = "quit", /* Mouse-specific actions */ [BIND_ACTION_SCROLLBACK_UP_MOUSE] = "scrollback-up-mouse", @@ -354,9 +356,9 @@ /* First, check XDG_CONFIG_HOME (or .config, if unset) */ if (xdg_config_home != NULL && xdg_config_home[0] != '\0') - path = xasprintf("%s/foot/foot.ini", xdg_config_home); + path = xstrjoin(xdg_config_home, "/foot/foot.ini"); else if (home_dir != NULL) - path = xasprintf("%s/.config/foot/foot.ini", home_dir); + path = xstrjoin(home_dir, "/.config/foot/foot.ini"); if (path != NULL) { LOG_DBG("checking for %s", path); @@ -381,7 +383,7 @@ conf_dir = strtok(NULL, ":")) { free(path); - path = xasprintf("%s/foot/foot.ini", conf_dir); + path = xstrjoin(conf_dir, "/foot/foot.ini"); LOG_DBG("checking for %s", path); int fd = open(path, O_RDONLY | O_CLOEXEC); @@ -539,7 +541,7 @@ * * - key="value" OK * - key=abc "quote" def NOT OK - * - key=’value’ OK + * - key='value' OK * * Finally, we support escaping the quote character, and the * escape character itself: @@ -618,18 +620,30 @@ } static bool NOINLINE -value_to_color(struct context *ctx, uint32_t *color, bool allow_alpha) +value_to_color(struct context *ctx, uint32_t *result, bool allow_alpha) { - if (!str_to_uint32(ctx->value, 16, color)) { - LOG_CONTEXTUAL_ERR("not a valid color value"); + uint32_t color; + const size_t len = strlen(ctx->value); + const size_t component_count = len / 2; + + if (!(len == 6 || (allow_alpha && len == 8)) || + !str_to_uint32(ctx->value, 16, &color)) + { + if (allow_alpha) { + LOG_CONTEXTUAL_ERR("color must be in either RGB or ARGB format"); + } else { + LOG_CONTEXTUAL_ERR("color must be in RGB format"); + } + return false; } - if (!allow_alpha && (*color & 0xff000000) != 0) { - LOG_CONTEXTUAL_ERR("color value must not have an alpha component"); - return false; + if (allow_alpha && component_count == 3) { + /* If user left out the alpha component, assume non-transparency */ + color |= 0xff000000; } + *result = color; return true; } @@ -812,7 +826,7 @@ const char *value = ctx->value; bool errors_are_fatal = ctx->errors_are_fatal; - if (strcmp(key, "include") == 0) { + if (streq(key, "include")) { char *_include_path = NULL; const char *include_path = NULL; @@ -852,25 +866,25 @@ return ret; } - else if (strcmp(key, "term") == 0) + else if (streq(key, "term")) return value_to_str(ctx, &conf->term); - else if (strcmp(key, "shell") == 0) + else if (streq(key, "shell")) return value_to_str(ctx, &conf->shell); - else if (strcmp(key, "login-shell") == 0) + else if (streq(key, "login-shell")) return value_to_bool(ctx, &conf->login_shell); - else if (strcmp(key, "title") == 0) + else if (streq(key, "title")) return value_to_str(ctx, &conf->title); - else if (strcmp(key, "locked-title") == 0) + else if (streq(key, "locked-title")) return value_to_bool(ctx, &conf->locked_title); - else if (strcmp(key, "app-id") == 0) + else if (streq(key, "app-id")) return value_to_str(ctx, &conf->app_id); - else if (strcmp(key, "initial-window-size-pixels") == 0) { + else if (streq(key, "initial-window-size-pixels")) { if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height)) return false; @@ -878,7 +892,7 @@ return true; } - else if (strcmp(key, "initial-window-size-chars") == 0) { + else if (streq(key, "initial-window-size-chars")) { if (!value_to_dimensions(ctx, &conf->size.width, &conf->size.height)) return false; @@ -886,7 +900,7 @@ return true; } - else if (strcmp(key, "pad") == 0) { + else if (streq(key, "pad")) { unsigned x, y; char mode[16] = {0}; @@ -906,11 +920,14 @@ return true; } - else if (strcmp(key, "resize-delay-ms") == 0) + else if (streq(key, "resize-delay-ms")) return value_to_uint16(ctx, 10, &conf->resize_delay_ms); - else if (strcmp(key, "bold-text-in-bright") == 0) { - if (strcmp(value, "palette-based") == 0) { + else if (streq(key, "resize-by-cells")) + return value_to_bool(ctx, &conf->resize_by_cells); + + else if (streq(key, "bold-text-in-bright")) { + if (streq(value, "palette-based")) { conf->bold_in_bright.enabled = true; conf->bold_in_bright.palette_based = true; } else { @@ -921,7 +938,7 @@ return true; } - else if (strcmp(key, "initial-window-mode") == 0) { + else if (streq(key, "initial-window-mode")) { _Static_assert(sizeof(conf->startup_mode) == sizeof(int), "enum is not 32-bit"); @@ -931,16 +948,16 @@ (int *)&conf->startup_mode); } - else if (strcmp(key, "font") == 0 || - strcmp(key, "font-bold") == 0 || - strcmp(key, "font-italic") == 0 || - strcmp(key, "font-bold-italic") == 0) + else if (streq(key, "font") || + streq(key, "font-bold") || + streq(key, "font-italic") || + streq(key, "font-bold-italic")) { size_t idx = - strcmp(key, "font") == 0 ? 0 : - strcmp(key, "font-bold") == 0 ? 1 : - strcmp(key, "font-italic") == 0 ? 2 : 3; + streq(key, "font") ? 0 : + streq(key, "font-bold") ? 1 : + streq(key, "font-italic") ? 2 : 3; struct config_font_list new_list = value_to_fonts(ctx); if (new_list.arr == NULL) @@ -951,7 +968,7 @@ return true; } - else if (strcmp(key, "font-size-adjustment") == 0) { + else if (streq(key, "font-size-adjustment")) { const size_t len = strlen(ctx->value); if (len >= 1 && ctx->value[len - 1] == '%') { errno = 0; @@ -976,44 +993,44 @@ } } - else if (strcmp(key, "line-height") == 0) + else if (streq(key, "line-height")) return value_to_pt_or_px(ctx, &conf->line_height); - else if (strcmp(key, "letter-spacing") == 0) + else if (streq(key, "letter-spacing")) return value_to_pt_or_px(ctx, &conf->letter_spacing); - else if (strcmp(key, "horizontal-letter-offset") == 0) + else if (streq(key, "horizontal-letter-offset")) return value_to_pt_or_px(ctx, &conf->horizontal_letter_offset); - else if (strcmp(key, "vertical-letter-offset") == 0) + else if (streq(key, "vertical-letter-offset")) return value_to_pt_or_px(ctx, &conf->vertical_letter_offset); - else if (strcmp(key, "underline-offset") == 0) { + else if (streq(key, "underline-offset")) { if (!value_to_pt_or_px(ctx, &conf->underline_offset)) return false; conf->use_custom_underline_offset = true; return true; } - else if (strcmp(key, "underline-thickness") == 0) + else if (streq(key, "underline-thickness")) return value_to_pt_or_px(ctx, &conf->underline_thickness); - else if (strcmp(key, "dpi-aware") == 0) + else if (streq(key, "dpi-aware")) return value_to_bool(ctx, &conf->dpi_aware); - else if (strcmp(key, "workers") == 0) + else if (streq(key, "workers")) return value_to_uint16(ctx, 10, &conf->render_worker_count); - else if (strcmp(key, "word-delimiters") == 0) + else if (streq(key, "word-delimiters")) return value_to_wchars(ctx, &conf->word_delimiters); - else if (strcmp(key, "notify") == 0) + else if (streq(key, "notify")) return value_to_spawn_template(ctx, &conf->notify); - else if (strcmp(key, "notify-focus-inhibit") == 0) + else if (streq(key, "notify-focus-inhibit")) return value_to_bool(ctx, &conf->notify_focus_inhibit); - else if (strcmp(key, "selection-target") == 0) { + else if (streq(key, "selection-target")) { _Static_assert(sizeof(conf->selection_target) == sizeof(int), "enum is not 32-bit"); @@ -1023,14 +1040,14 @@ (int *)&conf->selection_target); } - else if (strcmp(key, "box-drawings-uses-font-glyphs") == 0) + else if (streq(key, "box-drawings-uses-font-glyphs")) return value_to_bool(ctx, &conf->box_drawings_uses_font_glyphs); - else if (strcmp(key, "utmp-helper") == 0) { + else if (streq(key, "utmp-helper")) { if (!value_to_str(ctx, &conf->utmp_helper_path)) return false; - if (strcmp(conf->utmp_helper_path, "none") == 0) { + if (streq(conf->utmp_helper_path, "none")) { free(conf->utmp_helper_path); conf->utmp_helper_path = NULL; } @@ -1050,15 +1067,15 @@ struct config *conf = ctx->conf; const char *key = ctx->key; - if (strcmp(key, "urgent") == 0) + if (streq(key, "urgent")) return value_to_bool(ctx, &conf->bell.urgent); - else if (strcmp(key, "notify") == 0) + else if (streq(key, "notify")) return value_to_bool(ctx, &conf->bell.notify); - else if (strcmp(key, "visual") == 0) + else if (streq(key, "visual")) return value_to_bool(ctx, &conf->bell.flash); - else if (strcmp(key, "command") == 0) + else if (streq(key, "command")) return value_to_spawn_template(ctx, &conf->bell.command); - else if (strcmp(key, "command-focused") == 0) + else if (streq(key, "command-focused")) return value_to_bool(ctx, &conf->bell.command_focused); else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); @@ -1073,10 +1090,10 @@ const char *key = ctx->key; const char *value = ctx->value; - if (strcmp(key, "lines") == 0) + if (streq(key, "lines")) return value_to_uint32(ctx, 10, &conf->scrollback.lines); - else if (strcmp(key, "indicator-position") == 0) { + else if (streq(key, "indicator-position")) { _Static_assert( sizeof(conf->scrollback.indicator.position) == sizeof(int), "enum is not 32-bit"); @@ -1087,12 +1104,12 @@ (int *)&conf->scrollback.indicator.position); } - else if (strcmp(key, "indicator-format") == 0) { - if (strcmp(value, "percentage") == 0) { + else if (streq(key, "indicator-format")) { + if (streq(value, "percentage")) { conf->scrollback.indicator.format = SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE; return true; - } else if (strcmp(value, "line") == 0) { + } else if (streq(value, "line")) { conf->scrollback.indicator.format = SCROLLBACK_INDICATOR_FORMAT_LINENO; return true; @@ -1100,7 +1117,7 @@ return value_to_wchars(ctx, &conf->scrollback.indicator.text); } - else if (strcmp(key, "multiplier") == 0) + else if (streq(key, "multiplier")) return value_to_float(ctx, &conf->scrollback.multiplier); else { @@ -1116,13 +1133,13 @@ const char *key = ctx->key; const char *value = ctx->value; - if (strcmp(key, "launch") == 0) + if (streq(key, "launch")) return value_to_spawn_template(ctx, &conf->url.launch); - else if (strcmp(key, "label-letters") == 0) + else if (streq(key, "label-letters")) return value_to_wchars(ctx, &conf->url.label_letters); - else if (strcmp(key, "osc8-underline") == 0) { + else if (streq(key, "osc8-underline")) { _Static_assert(sizeof(conf->url.osc8_underline) == sizeof(int), "enum is not 32-bit"); @@ -1132,7 +1149,7 @@ (int *)&conf->url.osc8_underline); } - else if (strcmp(key, "protocols") == 0) { + else if (streq(key, "protocols")) { for (size_t i = 0; i < conf->url.prot_count; i++) free(conf->url.protocols[i]); free(conf->url.protocols); @@ -1184,7 +1201,7 @@ return true; } - else if (strcmp(key, "uri-characters") == 0) { + else if (streq(key, "uri-characters")) { if (!value_to_wchars(ctx, &conf->url.uri_characters)) return false; @@ -1239,13 +1256,13 @@ return true; } - else if (strcmp(key, "flash") == 0) color = &conf->colors.flash; - else if (strcmp(key, "foreground") == 0) color = &conf->colors.fg; - else if (strcmp(key, "background") == 0) color = &conf->colors.bg; - else if (strcmp(key, "selection-foreground") == 0) color = &conf->colors.selection_fg; - else if (strcmp(key, "selection-background") == 0) color = &conf->colors.selection_bg; + else if (streq(key, "flash")) color = &conf->colors.flash; + else if (streq(key, "foreground")) color = &conf->colors.fg; + else if (streq(key, "background")) color = &conf->colors.bg; + else if (streq(key, "selection-foreground")) color = &conf->colors.selection_fg; + else if (streq(key, "selection-background")) color = &conf->colors.selection_bg; - else if (strcmp(key, "jump-labels") == 0) { + else if (streq(key, "jump-labels")) { if (!value_to_two_colors( ctx, &conf->colors.jump_label.fg, @@ -1259,7 +1276,7 @@ return true; } - else if (strcmp(key, "scrollback-indicator") == 0) { + else if (streq(key, "scrollback-indicator")) { if (!value_to_two_colors( ctx, &conf->colors.scrollback_indicator.fg, @@ -1273,7 +1290,7 @@ return true; } - else if (strcmp(key, "search-box-no-match") == 0) { + else if (streq(key, "search-box-no-match")) { if (!value_to_two_colors( ctx, &conf->colors.search_box.no_match.fg, @@ -1287,7 +1304,7 @@ return true; } - else if (strcmp(key, "search-box-match") == 0) { + else if (streq(key, "search-box-match")) { if (!value_to_two_colors( ctx, &conf->colors.search_box.match.fg, @@ -1301,7 +1318,7 @@ return true; } - else if (strcmp(key, "urls") == 0) { + else if (streq(key, "urls")) { if (!value_to_color(ctx, &conf->colors.url, false)) return false; @@ -1309,7 +1326,7 @@ return true; } - else if (strcmp(key, "alpha") == 0) { + else if (streq(key, "alpha")) { float alpha; if (!value_to_float(ctx, &alpha)) return false; @@ -1323,7 +1340,7 @@ return true; } - else if (strcmp(key, "flash-alpha") == 0) { + else if (streq(key, "flash-alpha")) { float alpha; if (!value_to_float(ctx, &alpha)) return false; @@ -1357,7 +1374,7 @@ struct config *conf = ctx->conf; const char *key = ctx->key; - if (strcmp(key, "style") == 0) { + if (streq(key, "style")) { _Static_assert(sizeof(conf->cursor.style) == sizeof(int), "enum is not 32-bit"); @@ -1367,10 +1384,20 @@ (int *)&conf->cursor.style); } - else if (strcmp(key, "blink") == 0) + 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); - else if (strcmp(key, "color") == 0) { + else if (streq(key, "color")) { if (!value_to_two_colors( ctx, &conf->cursor.color.text, @@ -1385,10 +1412,10 @@ return true; } - else if (strcmp(key, "beam-thickness") == 0) + else if (streq(key, "beam-thickness")) return value_to_pt_or_px(ctx, &conf->cursor.beam_thickness); - else if (strcmp(key, "underline-thickness") == 0) + else if (streq(key, "underline-thickness")) return value_to_pt_or_px(ctx, &conf->cursor.underline_thickness); else { @@ -1403,10 +1430,10 @@ struct config *conf = ctx->conf; const char *key = ctx->key; - if (strcmp(key, "hide-when-typing") == 0) + if (streq(key, "hide-when-typing")) return value_to_bool(ctx, &conf->mouse.hide_when_typing); - else if (strcmp(key, "alternate-scroll-mode") == 0) + else if (streq(key, "alternate-scroll-mode")) return value_to_bool(ctx, &conf->mouse.alternate_scroll_mode); else { @@ -1421,7 +1448,7 @@ struct config *conf = ctx->conf; const char *key = ctx->key; - if (strcmp(key, "preferred") == 0) { + if (streq(key, "preferred")) { _Static_assert(sizeof(conf->csd.preferred) == sizeof(int), "enum is not 32-bit"); @@ -1431,7 +1458,7 @@ (int *)&conf->csd.preferred); } - else if (strcmp(key, "font") == 0) { + else if (streq(key, "font")) { struct config_font_list new_list = value_to_fonts(ctx); if (new_list.arr == NULL) return false; @@ -1441,7 +1468,7 @@ return true; } - else if (strcmp(key, "color") == 0) { + else if (streq(key, "color")) { uint32_t color; if (!value_to_color(ctx, &color, true)) return false; @@ -1451,13 +1478,13 @@ return true; } - else if (strcmp(key, "size") == 0) + else if (streq(key, "size")) return value_to_uint16(ctx, 10, &conf->csd.title_height); - else if (strcmp(key, "button-width") == 0) + else if (streq(key, "button-width")) return value_to_uint16(ctx, 10, &conf->csd.button_width); - else if (strcmp(key, "button-color") == 0) { + else if (streq(key, "button-color")) { if (!value_to_color(ctx, &conf->csd.color.buttons, true)) return false; @@ -1465,7 +1492,7 @@ return true; } - else if (strcmp(key, "button-minimize-color") == 0) { + else if (streq(key, "button-minimize-color")) { if (!value_to_color(ctx, &conf->csd.color.minimize, true)) return false; @@ -1473,7 +1500,7 @@ return true; } - else if (strcmp(key, "button-maximize-color") == 0) { + else if (streq(key, "button-maximize-color")) { if (!value_to_color(ctx, &conf->csd.color.maximize, true)) return false; @@ -1481,7 +1508,7 @@ return true; } - else if (strcmp(key, "button-close-color") == 0) { + else if (streq(key, "button-close-color")) { if (!value_to_color(ctx, &conf->csd.color.quit, true)) return false; @@ -1489,7 +1516,7 @@ return true; } - else if (strcmp(key, "border-color") == 0) { + else if (streq(key, "border-color")) { if (!value_to_color(ctx, &conf->csd.color.border, true)) return false; @@ -1497,13 +1524,13 @@ return true; } - else if (strcmp(key, "border-width") == 0) + else if (streq(key, "border-width")) return value_to_uint16(ctx, 10, &conf->csd.border_width_visible); - else if (strcmp(key, "hide-when-maximized") == 0) + else if (streq(key, "hide-when-maximized")) return value_to_bool(ctx, &conf->csd.hide_when_maximized); - else if (strcmp(key, "double-click-to-maximize") == 0) + else if (streq(key, "double-click-to-maximize")) return value_to_bool(ctx, &conf->csd.double_click_to_maximize); else { @@ -1529,6 +1556,7 @@ free_key_binding(struct config_key_binding *binding) { free_binding_aux(&binding->aux); + tll_free_and_free(binding->modifiers, free); } static void NOINLINE @@ -1544,43 +1572,26 @@ bindings->count = 0; } -static bool NOINLINE -parse_modifiers(struct context *ctx, const char *text, size_t len, - struct config_key_modifiers *modifiers) +static void NOINLINE +parse_modifiers(const char *text, size_t len, config_modifier_list_t *modifiers) { - bool ret = false; - - *modifiers = (struct config_key_modifiers){0}; + tll_free_and_free(*modifiers, free); /* Handle "none" separately because e.g. none+shift is nonsense */ if (strncmp(text, "none", len) == 0) - return true; + return; char *copy = xstrndup(text, len); - for (char *tok_ctx = NULL, *key = strtok_r(copy, "+", &tok_ctx); + for (char *ctx = NULL, *key = strtok_r(copy, "+", &ctx); key != NULL; - key = strtok_r(NULL, "+", &tok_ctx)) + key = strtok_r(NULL, "+", &ctx)) { - if (strcmp(key, XKB_MOD_NAME_SHIFT) == 0) - modifiers->shift = true; - else if (strcmp(key, XKB_MOD_NAME_CTRL) == 0) - modifiers->ctrl = true; - else if (strcmp(key, XKB_MOD_NAME_ALT) == 0) - modifiers->alt = true; - else if (strcmp(key, XKB_MOD_NAME_LOGO) == 0) - modifiers->super = true; - else { - LOG_CONTEXTUAL_ERR("not a valid modifier name: %s", key); - goto out; - } + tll_push_back(*modifiers, xstrdup(key)); } - ret = true; - -out: free(copy); - return ret; + tll_sort(*modifiers, strcmp); } static int NOINLINE @@ -1686,7 +1697,7 @@ mouse_button_name_to_code(const char *name) { for (size_t i = 0; i < ALEN(button_map); i++) { - if (strcmp(button_map[i].name, name) == 0) + if (streq(button_map[i].name, name)) return button_map[i].code; } return -1; @@ -1716,6 +1727,7 @@ /* 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, ' ')) @@ -1731,7 +1743,7 @@ for (char *tok_ctx = NULL, *combo = strtok_r(copy, " ", &tok_ctx); combo != NULL; combo = strtok_r(NULL, " ", &tok_ctx), - idx++) + idx++, used_combos++) { struct config_key_binding *new_combo = &new_combos[idx]; new_combo->action = action; @@ -1742,6 +1754,7 @@ 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; @@ -1750,11 +1763,9 @@ if (key == NULL) { /* No modifiers */ key = combo; - new_combo->modifiers = (struct config_key_modifiers){0}; } else { *key = '\0'; - if (!parse_modifiers(ctx, combo, key - combo, &new_combo->modifiers)) - goto err; + parse_modifiers(combo, key - combo, &new_combo->modifiers); key++; /* Skip past the '+' */ } @@ -1823,41 +1834,91 @@ return true; err: + if (idx > 0) { + for (size_t i = 0; i < used_combos; i++) + free_key_binding(&new_combos[i]); + } free(copy); return false; } static bool -modifiers_equal(const struct config_key_modifiers *mods1, - const struct config_key_modifiers *mods2) +modifiers_equal(const config_modifier_list_t *mods1, + const config_modifier_list_t *mods2) { - bool shift = mods1->shift == mods2->shift; - bool alt = mods1->alt == mods2->alt; - bool ctrl = mods1->ctrl == mods2->ctrl; - bool super = mods1->super == mods2->super; - return shift && alt && ctrl && super; + if (tll_length(*mods1) != tll_length(*mods2)) + return false; + + size_t count = 0; + tll_foreach(*mods1, it1) { + size_t skip = count; + tll_foreach(*mods2, it2) { + if (skip > 0) { + skip--; + continue; + } + + if (strcmp(it1->item, it2->item) != 0) + return false; + break; + } + + count++; + } + + return true; + /* + * bool shift = mods1->shift == mods2->shift; + * bool alt = mods1->alt == mods2->alt; + * bool ctrl = mods1->ctrl == mods2->ctrl; + * bool super = mods1->super == mods2->super; + * return shift && alt && ctrl && super; + */ +} + +UNITTEST +{ + config_modifier_list_t mods1 = tll_init(); + config_modifier_list_t mods2 = tll_init(); + + tll_push_back(mods1, xstrdup("foo")); + tll_push_back(mods1, xstrdup("bar")); + + tll_push_back(mods2, xstrdup("foo")); + xassert(!modifiers_equal(&mods1, &mods2)); + + tll_push_back(mods2, xstrdup("zoo")); + xassert(!modifiers_equal(&mods1, &mods2)); + + free(tll_pop_back(mods2)); + tll_push_back(mods2, xstrdup("bar")); + xassert(modifiers_equal(&mods1, &mods2)); + + tll_free_and_free(mods1, free); + tll_free_and_free(mods2, free); } static bool -modifiers_disjoint(const struct config_key_modifiers *mods1, - const struct config_key_modifiers *mods2) +modifiers_disjoint(const config_modifier_list_t *mods1, + const config_modifier_list_t *mods2) { - bool shift = mods1->shift && mods2->shift; - bool alt = mods1->alt && mods2->alt; - bool ctrl = mods1->ctrl && mods2->ctrl; - bool super = mods1->super && mods2->super; - return !(shift || alt || ctrl || super); + return !modifiers_equal(mods1, mods2); } static char * NOINLINE -modifiers_to_str(const struct config_key_modifiers *mods) +modifiers_to_str(const config_modifier_list_t *mods) { - char *ret = xasprintf( - "%s%s%s%s", - mods->ctrl ? XKB_MOD_NAME_CTRL "+" : "", - mods->alt ? XKB_MOD_NAME_ALT "+": "", - mods->super ? XKB_MOD_NAME_LOGO "+": "", - mods->shift ? XKB_MOD_NAME_SHIFT "+": ""); + size_t len = tll_length(*mods); /* '+' , and NULL terminator */ + tll_foreach(*mods, it) + len += strlen(it->item); + + char *ret = xmalloc(len); + size_t idx = 0; + tll_foreach(*mods, it) { + idx += snprintf(&ret[idx], len - idx, "%s", it->item); + ret[idx++] = '+'; + } + ret[--idx] = '\0'; return ret; } @@ -1935,7 +1996,7 @@ if (action_map[action] == NULL) continue; - if (strcmp(ctx->key, action_map[action]) != 0) + if (!streq(ctx->key, action_map[action])) continue; if (!value_to_key_combos(ctx, action, &aux, bindings, KEY_BINDING)) { @@ -1999,10 +2060,13 @@ xassert(bindings.arr[0].action == TEST_ACTION_FOO); xassert(bindings.arr[1].action == TEST_ACTION_BAR); xassert(bindings.arr[1].k.sym == XKB_KEY_g); - xassert(bindings.arr[1].modifiers.ctrl); + xassert(tll_length(bindings.arr[1].modifiers) == 1); + xassert(strcmp(tll_front(bindings.arr[1].modifiers), XKB_MOD_NAME_CTRL) == 0); xassert(bindings.arr[2].action == TEST_ACTION_BAR); xassert(bindings.arr[2].k.sym == XKB_KEY_x); - xassert(bindings.arr[2].modifiers.ctrl && bindings.arr[2].modifiers.shift); + xassert(tll_length(bindings.arr[2].modifiers) == 2); + xassert(strcmp(tll_front(bindings.arr[2].modifiers), XKB_MOD_NAME_CTRL) == 0); + xassert(strcmp(tll_back(bindings.arr[2].modifiers), XKB_MOD_NAME_SHIFT) == 0); /* * REPLACE foo with foo=Mod+v Shift+q @@ -2018,10 +2082,12 @@ xassert(bindings.arr[1].action == TEST_ACTION_BAR); xassert(bindings.arr[2].action == TEST_ACTION_FOO); xassert(bindings.arr[2].k.sym == XKB_KEY_v); - xassert(bindings.arr[2].modifiers.alt); + xassert(tll_length(bindings.arr[2].modifiers) == 1); + xassert(strcmp(tll_front(bindings.arr[2].modifiers), XKB_MOD_NAME_ALT) == 0); xassert(bindings.arr[3].action == TEST_ACTION_FOO); xassert(bindings.arr[3].k.sym == XKB_KEY_q); - xassert(bindings.arr[3].modifiers.shift); + xassert(tll_length(bindings.arr[3].modifiers) == 1); + xassert(strcmp(tll_front(bindings.arr[3].modifiers), XKB_MOD_NAME_SHIFT) == 0); /* * REMOVE bar @@ -2088,7 +2154,7 @@ struct config_key_binding *binding1 = &bindings->arr[i]; xassert(binding1->action != BIND_ACTION_NONE); - const struct config_key_modifiers *mods1 = &binding1->modifiers; + const config_modifier_list_t *mods1 = &binding1->modifiers; /* Does our modifiers collide with the selection override mods? */ if (type == MOUSE_BINDING && @@ -2112,7 +2178,7 @@ continue; } - const struct config_key_modifiers *mods2 = &binding2->modifiers; + const config_modifier_list_t *mods2 = &binding2->modifiers; bool mods_equal = modifiers_equal(mods1, mods2); bool sym_equal; @@ -2236,14 +2302,10 @@ const char *key = ctx->key; const char *value = ctx->value; - if (strcmp(key, "selection-override-modifiers") == 0) { - if (!parse_modifiers( - ctx, ctx->value, strlen(value), - &conf->mouse.selection_override_modifiers)) - { - LOG_CONTEXTUAL_ERR("%s: invalid modifiers '%s'", key, ctx->value); - return false; - } + if (streq(key, "selection-override-modifiers")) { + parse_modifiers( + ctx->value, strlen(value), + &conf->mouse.selection_override_modifiers); return true; } @@ -2263,7 +2325,7 @@ if (binding_action_map[action] == NULL) continue; - if (strcmp(key, binding_action_map[action]) != 0) + if (!streq(key, binding_action_map[action])) continue; if (!value_to_key_combos( @@ -2338,7 +2400,7 @@ struct binding_aux aux = { .type = BINDING_AUX_TEXT, .text = { - .data = data, + .data = data, /* data is now owned by value_to_key_combos() */ .len = data_len, }, }; @@ -2346,7 +2408,8 @@ if (!value_to_key_combos(ctx, BIND_ACTION_TEXT_BINDING, &aux, &conf->bindings.key, KEY_BINDING)) { - goto err; + /* Do *not* free(data) - it is handled by value_to_key_combos() */ + return false; } return true; @@ -2364,7 +2427,7 @@ /* Check for pre-existing env variable */ tll_foreach(conf->env_vars, it) { - if (strcmp(it->item.name, key) == 0) + if (streq(it->item.name, key)) return value_to_str(ctx, &it->item.value); } @@ -2386,7 +2449,7 @@ struct config *conf = ctx->conf; const char *key = ctx->key; - if (strcmp(key, "scaling-filter") == 0) { + if (streq(key, "scaling-filter")) { static const char *filters[] = { [FCFT_SCALING_FILTER_NONE] = "none", [FCFT_SCALING_FILTER_NEAREST] = "nearest", @@ -2402,13 +2465,13 @@ return value_to_enum(ctx, filters, (int *)&conf->tweak.fcft_filter); } - else if (strcmp(key, "overflowing-glyphs") == 0) + else if (streq(key, "overflowing-glyphs")) return value_to_bool(ctx, &conf->tweak.overflowing_glyphs); - else if (strcmp(key, "damage-whole-window") == 0) + else if (streq(key, "damage-whole-window")) return value_to_bool(ctx, &conf->tweak.damage_whole_window); - else if (strcmp(key, "grapheme-shaping") == 0) { + else if (streq(key, "grapheme-shaping")) { if (!value_to_bool(ctx, &conf->tweak.grapheme_shaping)) return false; @@ -2431,7 +2494,7 @@ return true; } - else if (strcmp(key, "grapheme-width-method") == 0) { + else if (streq(key, "grapheme-width-method")) { _Static_assert(sizeof(conf->tweak.grapheme_width_method) == sizeof(int), "enum is not 32-bit"); @@ -2441,7 +2504,7 @@ (int *)&conf->tweak.grapheme_width_method); } - else if (strcmp(key, "render-timer") == 0) { + else if (streq(key, "render-timer")) { _Static_assert(sizeof(conf->tweak.render_timer) == sizeof(int), "enum is not 32-bit"); @@ -2451,7 +2514,7 @@ (int *)&conf->tweak.render_timer); } - else if (strcmp(key, "delayed-render-lower") == 0) { + else if (streq(key, "delayed-render-lower")) { uint32_t ns; if (!value_to_uint32(ctx, 10, &ns)) return false; @@ -2465,7 +2528,7 @@ return true; } - else if (strcmp(key, "delayed-render-upper") == 0) { + else if (streq(key, "delayed-render-upper")) { uint32_t ns; if (!value_to_uint32(ctx, 10, &ns)) return false; @@ -2479,7 +2542,7 @@ return true; } - else if (strcmp(key, "max-shm-pool-size-mb") == 0) { + else if (streq(key, "max-shm-pool-size-mb")) { uint32_t mb; if (!value_to_uint32(ctx, 10, &mb)) return false; @@ -2488,19 +2551,19 @@ return true; } - else if (strcmp(key, "box-drawing-base-thickness") == 0) + else if (streq(key, "box-drawing-base-thickness")) return value_to_float(ctx, &conf->tweak.box_drawing_base_thickness); - else if (strcmp(key, "box-drawing-solid-shades") == 0) + else if (streq(key, "box-drawing-solid-shades")) return value_to_bool(ctx, &conf->tweak.box_drawing_solid_shades); - else if (strcmp(key, "font-monospace-warn") == 0) + else if (streq(key, "font-monospace-warn")) return value_to_bool(ctx, &conf->tweak.font_monospace_warn); - else if (strcmp(key, "sixel") == 0) + else if (streq(key, "sixel")) return value_to_bool(ctx, &conf->tweak.sixel); - else if (strcmp(key, "bold-text-in-bright-amount") == 0) + else if (streq(key, "bold-text-in-bright-amount")) return value_to_float(ctx, &conf->bold_in_bright.amount); else { @@ -2514,7 +2577,7 @@ struct config *conf = ctx->conf; const char *key = ctx->key; - if (strcmp(key, "long-press-delay") == 0) + if (streq(key, "long-press-delay")) return value_to_uint32(ctx, 10, &conf->touch.long_press_delay); else { @@ -2637,7 +2700,7 @@ str_to_section(const char *str) { for (enum section section = SECTION_MAIN; section < SECTION_COUNT; ++section) { - if (strcmp(str, section_info[section].name) == 0) + if (streq(str, section_info[section].name)) return section; } return SECTION_COUNT; @@ -2809,137 +2872,137 @@ const char *wayland_display = getenv("WAYLAND_DISPLAY"); if (wayland_display == NULL) { - return xasprintf("%s/foot.sock", xdg_runtime); + return xstrjoin(xdg_runtime, "/foot.sock"); } return xasprintf("%s/foot-%s.sock", xdg_runtime, wayland_display); } -#define m_none {0} -#define m_alt {.alt = true} -#define m_ctrl {.ctrl = true} -#define m_shift {.shift = true} -#define m_ctrl_shift {.ctrl = true, .shift = true} -#define m_ctrl_shift_alt {.ctrl = true, .shift = true, .alt = true} +static config_modifier_list_t +m(const char *text) +{ + config_modifier_list_t ret = tll_init(); + parse_modifiers(text, strlen(text), &ret); + return ret; +} static void add_default_key_bindings(struct config *conf) { - static const struct config_key_binding bindings[] = { - {BIND_ACTION_SCROLLBACK_UP_PAGE, m_shift, {{XKB_KEY_Prior}}}, - {BIND_ACTION_SCROLLBACK_DOWN_PAGE, m_shift, {{XKB_KEY_Next}}}, - {BIND_ACTION_CLIPBOARD_COPY, m_ctrl_shift, {{XKB_KEY_c}}}, - {BIND_ACTION_CLIPBOARD_COPY, m_none, {{XKB_KEY_XF86Copy}}}, - {BIND_ACTION_CLIPBOARD_PASTE, m_ctrl_shift, {{XKB_KEY_v}}}, - {BIND_ACTION_CLIPBOARD_PASTE, m_none, {{XKB_KEY_XF86Paste}}}, - {BIND_ACTION_PRIMARY_PASTE, m_shift, {{XKB_KEY_Insert}}}, - {BIND_ACTION_SEARCH_START, m_ctrl_shift, {{XKB_KEY_r}}}, - {BIND_ACTION_FONT_SIZE_UP, m_ctrl, {{XKB_KEY_plus}}}, - {BIND_ACTION_FONT_SIZE_UP, m_ctrl, {{XKB_KEY_equal}}}, - {BIND_ACTION_FONT_SIZE_UP, m_ctrl, {{XKB_KEY_KP_Add}}}, - {BIND_ACTION_FONT_SIZE_DOWN, m_ctrl, {{XKB_KEY_minus}}}, - {BIND_ACTION_FONT_SIZE_DOWN, m_ctrl, {{XKB_KEY_KP_Subtract}}}, - {BIND_ACTION_FONT_SIZE_RESET, m_ctrl, {{XKB_KEY_0}}}, - {BIND_ACTION_FONT_SIZE_RESET, m_ctrl, {{XKB_KEY_KP_0}}}, - {BIND_ACTION_SPAWN_TERMINAL, m_ctrl_shift, {{XKB_KEY_n}}}, - {BIND_ACTION_SHOW_URLS_LAUNCH, m_ctrl_shift, {{XKB_KEY_o}}}, - {BIND_ACTION_UNICODE_INPUT, m_ctrl_shift, {{XKB_KEY_u}}}, - {BIND_ACTION_PROMPT_PREV, m_ctrl_shift, {{XKB_KEY_z}}}, - {BIND_ACTION_PROMPT_NEXT, m_ctrl_shift, {{XKB_KEY_x}}}, + const struct config_key_binding bindings[] = { + {BIND_ACTION_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Prior}}}, + {BIND_ACTION_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Next}}}, + {BIND_ACTION_CLIPBOARD_COPY, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_c}}}, + {BIND_ACTION_CLIPBOARD_COPY, m("none"), {{XKB_KEY_XF86Copy}}}, + {BIND_ACTION_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_v}}}, + {BIND_ACTION_CLIPBOARD_PASTE, m("none"), {{XKB_KEY_XF86Paste}}}, + {BIND_ACTION_PRIMARY_PASTE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Insert}}}, + {BIND_ACTION_SEARCH_START, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_r}}}, + {BIND_ACTION_FONT_SIZE_UP, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_plus}}}, + {BIND_ACTION_FONT_SIZE_UP, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_equal}}}, + {BIND_ACTION_FONT_SIZE_UP, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_Add}}}, + {BIND_ACTION_FONT_SIZE_DOWN, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_minus}}}, + {BIND_ACTION_FONT_SIZE_DOWN, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_Subtract}}}, + {BIND_ACTION_FONT_SIZE_RESET, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_0}}}, + {BIND_ACTION_FONT_SIZE_RESET, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_KP_0}}}, + {BIND_ACTION_SPAWN_TERMINAL, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_n}}}, + {BIND_ACTION_SHOW_URLS_LAUNCH, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_o}}}, + {BIND_ACTION_UNICODE_INPUT, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_u}}}, + {BIND_ACTION_PROMPT_PREV, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_z}}}, + {BIND_ACTION_PROMPT_NEXT, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_x}}}, }; conf->bindings.key.count = ALEN(bindings); - conf->bindings.key.arr = xmalloc(sizeof(bindings)); - memcpy(conf->bindings.key.arr, bindings, sizeof(bindings)); + conf->bindings.key.arr = xmemdup(bindings, sizeof(bindings)); } static void add_default_search_bindings(struct config *conf) { - static const struct config_key_binding bindings[] = { - {BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, m_shift, {{XKB_KEY_Prior}}}, - {BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, m_shift, {{XKB_KEY_Next}}}, - {BIND_ACTION_SEARCH_CANCEL, m_ctrl, {{XKB_KEY_c}}}, - {BIND_ACTION_SEARCH_CANCEL, m_ctrl, {{XKB_KEY_g}}}, - {BIND_ACTION_SEARCH_CANCEL, m_none, {{XKB_KEY_Escape}}}, - {BIND_ACTION_SEARCH_COMMIT, m_none, {{XKB_KEY_Return}}}, - {BIND_ACTION_SEARCH_FIND_PREV, m_ctrl, {{XKB_KEY_r}}}, - {BIND_ACTION_SEARCH_FIND_NEXT, m_ctrl, {{XKB_KEY_s}}}, - {BIND_ACTION_SEARCH_EDIT_LEFT, m_none, {{XKB_KEY_Left}}}, - {BIND_ACTION_SEARCH_EDIT_LEFT, m_ctrl, {{XKB_KEY_b}}}, - {BIND_ACTION_SEARCH_EDIT_LEFT_WORD, m_ctrl, {{XKB_KEY_Left}}}, - {BIND_ACTION_SEARCH_EDIT_LEFT_WORD, m_alt, {{XKB_KEY_b}}}, - {BIND_ACTION_SEARCH_EDIT_RIGHT, m_none, {{XKB_KEY_Right}}}, - {BIND_ACTION_SEARCH_EDIT_RIGHT, m_ctrl, {{XKB_KEY_f}}}, - {BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, m_ctrl, {{XKB_KEY_Right}}}, - {BIND_ACTION_SEARCH_EDIT_RIGHT_WORD, m_alt, {{XKB_KEY_f}}}, - {BIND_ACTION_SEARCH_EDIT_HOME, m_none, {{XKB_KEY_Home}}}, - {BIND_ACTION_SEARCH_EDIT_HOME, m_ctrl, {{XKB_KEY_a}}}, - {BIND_ACTION_SEARCH_EDIT_END, m_none, {{XKB_KEY_End}}}, - {BIND_ACTION_SEARCH_EDIT_END, m_ctrl, {{XKB_KEY_e}}}, - {BIND_ACTION_SEARCH_DELETE_PREV, m_none, {{XKB_KEY_BackSpace}}}, - {BIND_ACTION_SEARCH_DELETE_PREV_WORD, m_ctrl, {{XKB_KEY_BackSpace}}}, - {BIND_ACTION_SEARCH_DELETE_PREV_WORD, m_alt, {{XKB_KEY_BackSpace}}}, - {BIND_ACTION_SEARCH_DELETE_NEXT, m_none, {{XKB_KEY_Delete}}}, - {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m_ctrl, {{XKB_KEY_Delete}}}, - {BIND_ACTION_SEARCH_DELETE_NEXT_WORD, m_alt, {{XKB_KEY_d}}}, - {BIND_ACTION_SEARCH_EXTEND_CHAR, m_shift, {{XKB_KEY_Right}}}, - {BIND_ACTION_SEARCH_EXTEND_WORD, m_ctrl, {{XKB_KEY_w}}}, - {BIND_ACTION_SEARCH_EXTEND_WORD, m_ctrl_shift, {{XKB_KEY_Right}}}, - {BIND_ACTION_SEARCH_EXTEND_WORD_WS, m_ctrl_shift, {{XKB_KEY_w}}}, - {BIND_ACTION_SEARCH_EXTEND_LINE_DOWN, m_shift, {{XKB_KEY_Down}}}, - {BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR, m_shift, {{XKB_KEY_Left}}}, - {BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD, m_ctrl_shift, {{XKB_KEY_Left}}}, - {BIND_ACTION_SEARCH_EXTEND_LINE_UP, m_shift, {{XKB_KEY_Up}}}, - {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_ctrl, {{XKB_KEY_v}}}, - {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_ctrl_shift, {{XKB_KEY_v}}}, - {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_ctrl, {{XKB_KEY_y}}}, - {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m_none, {{XKB_KEY_XF86Paste}}}, - {BIND_ACTION_SEARCH_PRIMARY_PASTE, m_shift, {{XKB_KEY_Insert}}}, + const struct config_key_binding bindings[] = { + {BIND_ACTION_SEARCH_SCROLLBACK_UP_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Prior}}}, + {BIND_ACTION_SEARCH_SCROLLBACK_DOWN_PAGE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_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_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_EXTEND_CHAR, m(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, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Right}}}, + {BIND_ACTION_SEARCH_EXTEND_WORD, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_w}}}, + {BIND_ACTION_SEARCH_EXTEND_WORD_WS, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_w}}}, + {BIND_ACTION_SEARCH_EXTEND_LINE_DOWN, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Down}}}, + {BIND_ACTION_SEARCH_EXTEND_BACKWARD_CHAR, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Left}}}, + {BIND_ACTION_SEARCH_EXTEND_BACKWARD_WORD, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_Left}}}, + {BIND_ACTION_SEARCH_EXTEND_LINE_UP, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Up}}}, + {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_v}}}, + {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL "+" XKB_MOD_NAME_SHIFT), {{XKB_KEY_v}}}, + {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_y}}}, + {BIND_ACTION_SEARCH_CLIPBOARD_PASTE, m("none"), {{XKB_KEY_XF86Paste}}}, + {BIND_ACTION_SEARCH_PRIMARY_PASTE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_Insert}}}, }; conf->bindings.search.count = ALEN(bindings); - conf->bindings.search.arr = xmalloc(sizeof(bindings)); - memcpy(conf->bindings.search.arr, bindings, sizeof(bindings)); + conf->bindings.search.arr = xmemdup(bindings, sizeof(bindings)); } static void add_default_url_bindings(struct config *conf) { - static const struct config_key_binding bindings[] = { - {BIND_ACTION_URL_CANCEL, m_ctrl, {{XKB_KEY_c}}}, - {BIND_ACTION_URL_CANCEL, m_ctrl, {{XKB_KEY_g}}}, - {BIND_ACTION_URL_CANCEL, m_ctrl, {{XKB_KEY_d}}}, - {BIND_ACTION_URL_CANCEL, m_none, {{XKB_KEY_Escape}}}, - {BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL, m_none, {{XKB_KEY_t}}}, + const struct config_key_binding bindings[] = { + {BIND_ACTION_URL_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_c}}}, + {BIND_ACTION_URL_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_g}}}, + {BIND_ACTION_URL_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_d}}}, + {BIND_ACTION_URL_CANCEL, m("none"), {{XKB_KEY_Escape}}}, + {BIND_ACTION_URL_TOGGLE_URL_ON_JUMP_LABEL, m("none"), {{XKB_KEY_t}}}, }; conf->bindings.url.count = ALEN(bindings); - conf->bindings.url.arr = xmalloc(sizeof(bindings)); - memcpy(conf->bindings.url.arr, bindings, sizeof(bindings)); + conf->bindings.url.arr = xmemdup(bindings, sizeof(bindings)); } static void add_default_mouse_bindings(struct config *conf) { - static const struct config_key_binding bindings[] = { - {BIND_ACTION_SCROLLBACK_UP_MOUSE, m_none, {.m = {BTN_BACK, 1}}}, - {BIND_ACTION_SCROLLBACK_DOWN_MOUSE, m_none, {.m = {BTN_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_ctrl, {.m = {BTN_LEFT, 1}}}, - {BIND_ACTION_SELECT_EXTEND, m_none, {.m = {BTN_RIGHT, 1}}}, - {BIND_ACTION_SELECT_EXTEND_CHAR_WISE, m_ctrl, {.m = {BTN_RIGHT, 1}}}, - {BIND_ACTION_SELECT_WORD, m_none, {.m = {BTN_LEFT, 2}}}, - {BIND_ACTION_SELECT_WORD_WS, m_ctrl, {.m = {BTN_LEFT, 2}}}, - {BIND_ACTION_SELECT_QUOTE, m_none, {.m = {BTN_LEFT, 3}}}, - {BIND_ACTION_SELECT_ROW, m_none, {.m = {BTN_LEFT, 4}}}, + const struct config_key_binding bindings[] = { + {BIND_ACTION_SCROLLBACK_UP_MOUSE, m("none"), {.m = {BTN_BACK, 1}}}, + {BIND_ACTION_SCROLLBACK_DOWN_MOUSE, m("none"), {.m = {BTN_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_BACK, 1}}}, + {BIND_ACTION_FONT_SIZE_DOWN, m("Control"), {.m = {BTN_FORWARD, 1}}}, }; conf->bindings.mouse.count = ALEN(bindings); - conf->bindings.mouse.arr = xmalloc(sizeof(bindings)); - memcpy(conf->bindings.mouse.arr, bindings, sizeof(bindings)); + conf->bindings.mouse.arr = xmemdup(bindings, sizeof(bindings)); } static void NOINLINE @@ -2978,6 +3041,7 @@ }, .pad_x = 0, .pad_y = 0, + .resize_by_cells = true, .resize_delay_ms = 100, .bold_in_bright = { .enabled = false, @@ -3037,6 +3101,7 @@ .cursor = { .style = CURSOR_BLOCK, + .unfocused_style = CURSOR_UNFOCUSED_HOLLOW, .blink = false, .color = { .text = 0, @@ -3048,12 +3113,7 @@ .mouse = { .hide_when_typing = false, .alternate_scroll_mode = true, - .selection_override_modifiers = { - .shift = true, - .alt = false, - .ctrl = false, - .super = false, - }, + .selection_override_modifiers = tll_init(), }, .csd = { .preferred = CONF_CSD_PREFER_SERVER, @@ -3109,6 +3169,7 @@ }; memcpy(conf->colors.table, default_color_table, sizeof(default_color_table)); + parse_modifiers(XKB_MOD_NAME_SHIFT, 5, &conf->mouse.selection_override_modifiers); tokenize_cmdline("notify-send -a ${app-id} -i ${app-id} ${title} ${body}", &conf->notify.argv.args); @@ -3183,6 +3244,7 @@ ret = !errors_are_fatal; fclose(f); + conf_file.fd = -1; } } @@ -3200,7 +3262,7 @@ ret = false; } else { conf->fonts[0].count = 1; - conf->fonts[0].arr = malloc(sizeof(font)); + conf->fonts[0].arr = xmalloc(sizeof(font)); conf->fonts[0].arr[0] = font; } } @@ -3307,6 +3369,9 @@ 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: @@ -3331,8 +3396,7 @@ if (old->aux.master_copy) { const size_t len = old->aux.text.len; new->aux.text.len = len; - new->aux.text.data = xmalloc(len); - memcpy(new->aux.text.data, old->aux.text.data, len); + new->aux.text.data = xmemdup(old->aux.text.data, len); last_master_text_len = len; last_master_text_data = new->aux.text.data; @@ -3380,6 +3444,13 @@ 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), @@ -3412,13 +3483,13 @@ bool ret = config_load(&original, "/dev/null", ¬s, &overrides, false, false); xassert(ret); - struct config *clone = config_clone(&original); - xassert(clone != NULL); - xassert(clone != &original); + //struct config *clone = config_clone(&original); + //xassert(clone != NULL); + //xassert(clone != &original); config_free(&original); - config_free(clone); - free(clone); + //config_free(clone); + //free(clone); fcft_fini(); @@ -3454,6 +3525,7 @@ 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); @@ -3474,7 +3546,7 @@ /* * First look for user specified {pixel}size option - * e.g. “font-name:size=12” + * e.g. "font-name:size=12" */ double pt_size = -1.0; @@ -3485,9 +3557,9 @@ if (have_pt_size != FcResultMatch && have_px_size != FcResultMatch) { /* - * Apply fontconfig config. Can’t do that until we’ve first + * Apply fontconfig config. Can't do that until we've first * checked for a user provided size, since we may end up with - * both “size” and “pixelsize” being set, and we don’t know + * both "size" and "pixelsize" being set, and we don't know * which one takes priority. */ FcPattern *pat_copy = FcPatternDuplicate(pat); @@ -3584,6 +3656,7 @@ return is_monospaced; } +#if 0 xkb_mod_mask_t conf_modifiers_to_mask(const struct seat *seat, const struct config_key_modifiers *modifiers) @@ -3599,3 +3672,4 @@ mods |= modifiers->super << seat->kbd.mod_super; return mods; } +#endif diff -Nru foot-1.16.2/config.h foot-1.17.2/config.h --- foot-1.16.2/config.h 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/config.h 2024-04-17 09:26:45.000000000 +0000 @@ -28,6 +28,11 @@ }; enum cursor_style { CURSOR_BLOCK, CURSOR_UNDERLINE, CURSOR_BEAM }; +enum cursor_unfocused_style { + CURSOR_UNFOCUSED_UNCHANGED, + CURSOR_UNFOCUSED_HOLLOW, + CURSOR_UNFOCUSED_NONE +}; enum conf_size_type {CONF_SIZE_PX, CONF_SIZE_CELLS}; @@ -38,12 +43,14 @@ }; DEFINE_LIST(struct config_font); +#if 0 struct config_key_modifiers { bool shift; bool alt; bool ctrl; bool super; }; +#endif struct argv { char **args; @@ -74,9 +81,12 @@ MOUSE_BINDING, }; +typedef tll(char *) config_modifier_list_t; + struct config_key_binding { int action; /* One of the varios bind_action_* enums from wayland.h */ - struct config_key_modifiers modifiers; + //struct config_key_modifiers modifiers; + config_modifier_list_t modifiers; union { /* Key bindings */ struct { @@ -128,6 +138,9 @@ unsigned pad_x; unsigned pad_y; bool center; + + bool resize_by_cells; + uint16_t resize_delay_ms; struct { @@ -248,6 +261,7 @@ struct { enum cursor_style style; + enum cursor_unfocused_style unfocused_style; bool blink; struct { uint32_t text; @@ -260,7 +274,8 @@ struct { bool hide_when_typing; bool alternate_scroll_mode; - struct config_key_modifiers selection_override_modifiers; + //struct config_key_modifiers selection_override_modifiers; + config_modifier_list_t selection_override_modifiers; } mouse; struct { @@ -302,7 +317,7 @@ uint32_t buttons; uint32_t minimize; uint32_t maximize; - uint32_t quit; /* ‘close’ collides with #define in epoll-shim */ + uint32_t quit; /* 'close' collides with #define in epoll-shim */ uint32_t border; } color; @@ -372,10 +387,11 @@ 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 -Nru foot-1.16.2/csi.c foot-1.17.2/csi.c --- foot-1.16.2/csi.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/csi.c 2024-04-17 09:26:45.000000000 +0000 @@ -324,6 +324,11 @@ 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; @@ -555,6 +560,7 @@ 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; @@ -600,6 +606,7 @@ 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; @@ -642,6 +649,7 @@ 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; @@ -673,6 +681,24 @@ 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) { @@ -743,10 +769,10 @@ * Note: tertiary DA responds with "FOOT". */ if (term->conf->tweak.sixel) { - static const char reply[] = "\033[?62;4;22c"; + static const char reply[] = "\033[?62;4;22;28c"; term_to_slave(term, reply, sizeof(reply) - 1); } else { - static const char reply[] = "\033[?62;22c"; + static const char reply[] = "\033[?62;22;28c"; term_to_slave(term, reply, sizeof(reply) - 1); } break; @@ -1081,44 +1107,29 @@ break; case 'h': - /* Set mode */ - switch (vt_param_get(term, 0, 0)) { - case 2: /* Keyboard Action Mode - AM */ - LOG_WARN("unimplemented: keyboard action mode (AM)"); - break; - - case 4: /* Insert Mode - IRM */ - term->insert_mode = true; + case 'l': { + /* Set/Reset Mode (SM/RM) */ + int param = vt_param_get(term, 0, 0); + bool sm = final == 'h'; + if (param == 4) { + /* Insertion Replacement Mode (IRM) */ + term->insert_mode = sm; term_update_ascii_printer(term); break; - - case 12: /* Send/receive Mode - SRM */ - LOG_WARN("unimplemented: send/receive mode (SRM)"); - break; - - case 20: /* Automatic Newline Mode - LNM */ - /* TODO: would be easy to implemented; when active - * term_linefeed() would _also_ do a - * term_carriage_return() */ - LOG_WARN("unimplemented: automatic newline mode (LNM)"); - break; } - break; - - case 'l': - /* Reset mode */ - switch (vt_param_get(term, 0, 0)) { - case 4: /* Insert Mode - IRM */ - term->insert_mode = false; - term_update_ascii_printer(term); - break; - case 2: /* Keyboard Action Mode - AM */ - case 12: /* Send/receive Mode - SRM */ - case 20: /* Automatic Newline Mode - LNM */ - break; + /* + * ECMA-48 defines modes 1-22, all of which were optional + * (§7.1; "may have one state only") and are considered + * deprecated (§7.1) in the latest (5th) edition. xterm only + * documents modes 2, 4, 12 and 20, the last of which was + * outright removed (§8.3.106) in 5th edition ECMA-48. + */ + if (sm) { + LOG_WARN("SM with unimplemented mode: %d", param); } break; + } case 'r': { int start = vt_param_get(term, 0, 1); @@ -1216,9 +1227,7 @@ if (width >= 0 && height >= 0) { char reply[64]; size_t n = xsnprintf( - reply, sizeof(reply), "\033[4;%d;%dt", - (int)roundf(height / term->scale), - (int)roundf((width / term->scale))); + reply, sizeof(reply), "\033[4;%d;%dt", height, width); term_to_slave(term, reply, n); } break; @@ -1228,8 +1237,8 @@ tll_foreach(term->window->on_outputs, it) { char reply[64]; size_t n = xsnprintf(reply, sizeof(reply), "\033[5;%d;%dt", - it->item->dim.px_scaled.height, - it->item->dim.px_scaled.width); + it->item->dim.px_real.height, + it->item->dim.px_real.width); term_to_slave(term, reply, n); break; } @@ -1242,8 +1251,7 @@ char reply[64]; size_t n = xsnprintf( reply, sizeof(reply), "\033[6;%d;%dt", - (int)roundf(term->cell_height / term->scale), - (int)roundf(term->cell_width / term->scale)); + term->cell_height, term->cell_width); term_to_slave(term, reply, n); break; } @@ -1261,8 +1269,8 @@ char reply[64]; size_t n = xsnprintf( reply, sizeof(reply), "\033[9;%d;%dt", - (int)roundf(it->item->dim.px_real.height / term->cell_height / term->scale), - (int)roundf(it->item->dim.px_real.width / term->cell_width / term->scale)); + 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; } @@ -1444,6 +1452,23 @@ 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]; @@ -1551,8 +1576,8 @@ break; case 4: /* modifyOtherKeys */ - /* We don’t support fully disabling modifyOtherKeys, - * but simply revert back to mode ‘1’ */ + /* We don't support fully disabling modifyOtherKeys, + * but simply revert back to mode '1' */ term->modify_other_keys_2 = false; LOG_DBG("modifyOtherKeys=1"); break; @@ -1630,7 +1655,7 @@ break; } } - break; /* private[0] == ‘<’ */ + break; /* private[0] == '<' */ } case ' ': { @@ -1750,6 +1775,214 @@ 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 0x243f: /* ?$ */ switch (final) { case 'p': { @@ -1777,7 +2010,7 @@ break; } - break; /* private[0] == ‘?’ && private[1] == ‘$’ */ + break; /* private[0] == '?' && private[1] == '$' */ default: UNHANDLED(); diff -Nru foot-1.16.2/cursor-shape.c foot-1.17.2/cursor-shape.c --- foot-1.16.2/cursor-shape.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/cursor-shape.c 2024-04-17 09:26:45.000000000 +0000 @@ -9,28 +9,26 @@ #include "debug.h" #include "util.h" -const char * +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", - [CURSOR_SHAPE_LEFT_PTR] = "left_ptr", - [CURSOR_SHAPE_TEXT] = "text", - [CURSOR_SHAPE_TEXT_FALLBACK] = "xterm", - [CURSOR_SHAPE_TOP_LEFT_CORNER] = "top_left_corner", - [CURSOR_SHAPE_TOP_RIGHT_CORNER] = "top_right_corner", - [CURSOR_SHAPE_BOTTOM_LEFT_CORNER] = "bottom_left_corner", - [CURSOR_SHAPE_BOTTOM_RIGHT_CORNER] = "bottom_right_corner", - [CURSOR_SHAPE_LEFT_SIDE] = "left_side", - [CURSOR_SHAPE_RIGHT_SIDE] = "right_side", - [CURSOR_SHAPE_TOP_SIDE] = "top_side", - [CURSOR_SHAPE_BOTTOM_SIDE] = "bottom_side", + static const char *const table[][CURSOR_SHAPE_COUNT]= { + [CURSOR_SHAPE_NONE] = {NULL}, + [CURSOR_SHAPE_HIDDEN] = {"hidden", NULL}, + [CURSOR_SHAPE_LEFT_PTR] = {"default", "left_ptr", NULL}, + [CURSOR_SHAPE_TEXT] = {"text", "xterm", NULL}, + [CURSOR_SHAPE_TOP_LEFT_CORNER] = {"nw-resize", "top_left_corner", NULL}, + [CURSOR_SHAPE_TOP_RIGHT_CORNER] = {"ne-resize", "top_right_corner", NULL}, + [CURSOR_SHAPE_BOTTOM_LEFT_CORNER] = {"sw-resize", "bottom_left_corner", NULL}, + [CURSOR_SHAPE_BOTTOM_RIGHT_CORNER] = {"se-resize", "bottom_right_corner", NULL}, + [CURSOR_SHAPE_LEFT_SIDE] = {"w-resize", "left_side", NULL}, + [CURSOR_SHAPE_RIGHT_SIDE] = {"e-resize", "right_side", NULL}, + [CURSOR_SHAPE_TOP_SIDE] = {"n-resize", "top_side", NULL}, + [CURSOR_SHAPE_BOTTOM_SIDE] = {"s-resize", "bottom_side", NULL}, }; xassert(shape <= ALEN(table)); - xassert(table[shape] != NULL); return table[shape]; } @@ -40,7 +38,6 @@ static const enum wp_cursor_shape_device_v1_shape table[CURSOR_SHAPE_COUNT] = { [CURSOR_SHAPE_LEFT_PTR] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT, [CURSOR_SHAPE_TEXT] = WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT, - [CURSOR_SHAPE_TEXT_FALLBACK] = 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, @@ -101,7 +98,7 @@ for (size_t i = 0; i < ALEN(table); i++) { for (size_t j = 0; j < ALEN(table[i]); j++) { - if (table[i][j] != NULL && strcmp(xcursor, table[i][j]) == 0) { + if (table[i][j] != NULL && streq(xcursor, table[i][j])) { return i; } } diff -Nru foot-1.16.2/cursor-shape.h foot-1.17.2/cursor-shape.h --- foot-1.16.2/cursor-shape.h 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/cursor-shape.h 2024-04-17 09:26:45.000000000 +0000 @@ -9,7 +9,6 @@ CURSOR_SHAPE_LEFT_PTR, CURSOR_SHAPE_TEXT, - CURSOR_SHAPE_TEXT_FALLBACK, CURSOR_SHAPE_TOP_LEFT_CORNER, CURSOR_SHAPE_TOP_RIGHT_CORNER, CURSOR_SHAPE_BOTTOM_LEFT_CORNER, @@ -22,7 +21,7 @@ CURSOR_SHAPE_COUNT, }; -const char *cursor_shape_to_string(enum cursor_shape shape); +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); diff -Nru foot-1.16.2/dcs.c foot-1.17.2/dcs.c --- foot-1.16.2/dcs.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/dcs.c 2024-04-17 09:26:45.000000000 +0000 @@ -138,12 +138,12 @@ /* * Reply format: * \EP 1 + r cap=value \E\\ - * Where ‘cap’ and ‘value are hex encoded ascii strings + * Where 'cap' and 'value are hex encoded ascii strings */ char *reply = xmalloc( 5 + /* DCS 1 + r (\EP1+r) */ len + /* capability name, hex encoded */ - 1 + /* ‘=’ */ + 1 + /* '=' */ strlen(value) * 2 + /* capability value, hex encoded */ 2 + /* ST (\E\\) */ 1); @@ -239,7 +239,7 @@ return; struct vt *vt = &term->vt; - if (vt->dcs.idx > 2) + if (vt->dcs.idx >= 2) return; vt->dcs.data[vt->dcs.idx++] = c; } @@ -253,8 +253,8 @@ /* * A note on the Ps parameter in the reply: many DEC manual * instances (e.g. https://vt100.net/docs/vt510-rm/DECRPSS) claim - * that 0 means “request is valid”, and 1 means “request is - * invalid”. + * that 0 means "request is valid", and 1 means "request is + * invalid". * * However, this appears to be a typo; actual hardware inverts the * response (as does XTerm and mlterm): diff -Nru foot-1.16.2/debian/changelog foot-1.17.2/debian/changelog --- foot-1.16.2/debian/changelog 2024-04-08 14:58:35.000000000 +0000 +++ foot-1.17.2/debian/changelog 2024-04-26 09:55:44.000000000 +0000 @@ -1,8 +1,28 @@ -foot (1.16.2-2build1) noble; urgency=high +foot (1.17.2-2) unstable; urgency=medium - * No change rebuild against libfcft4t64. + * Fix d/watch by switching to git mode - -- Julian Andres Klode Mon, 08 Apr 2024 16:58:35 +0200 + -- Birger Schacht Fri, 26 Apr 2024 11:55:44 +0200 + +foot (1.17.2-1) unstable; urgency=medium + + * New usptream release + + -- Birger Schacht Wed, 17 Apr 2024 16:11:34 +0200 + +foot (1.17.1-1) unstable; urgency=medium + + * New upstream release + + -- Birger Schacht Sun, 14 Apr 2024 21:15:50 +0200 + +foot (1.17.0-1) unstable; urgency=medium + + * New upstream release + * d/control: replace build-dep on pkg-config with pkgconf + * d/patches: update pgo verbose patch + + -- Birger Schacht Wed, 03 Apr 2024 17:23:25 +0200 foot (1.16.2-2) unstable; urgency=medium diff -Nru foot-1.16.2/debian/control foot-1.17.2/debian/control --- foot-1.16.2/debian/control 2024-04-08 14:58:35.000000000 +0000 +++ foot-1.17.2/debian/control 2024-04-26 09:55:44.000000000 +0000 @@ -1,14 +1,13 @@ Source: foot Section: x11 Priority: optional -Maintainer: Ubuntu Developers -XSBC-Original-Maintainer: Birger Schacht +Maintainer: Birger Schacht Build-Depends: debhelper-compat (= 13), meson (>= 0.59), ninja-build, wayland-protocols, libxkbcommon-dev, - pkg-config, + pkgconf, libpixman-1-dev, libfreetype-dev, libfontconfig-dev, diff -Nru foot-1.16.2/debian/patches/verbose-pgo.patch foot-1.17.2/debian/patches/verbose-pgo.patch --- foot-1.16.2/debian/patches/verbose-pgo.patch 2024-01-13 10:11:42.000000000 +0000 +++ foot-1.17.2/debian/patches/verbose-pgo.patch 2024-04-26 09:55:44.000000000 +0000 @@ -1,13 +1,18 @@ -Description: Build verbosely in PGO script -Author: Andrea Pappacoda -Forwarded: not-needed -Last-Update: 2022-10-23 +From eb1f31adf4ca8afa7889481b0fdac84dc1a35db7 Mon Sep 17 00:00:00 2001 +From: Birger Schacht +Date: Wed, 3 Apr 2024 17:28:59 +0200 +Subject: [PATCH] Build verbosely in PGO script + +Update patch to build verbosely in PGO script for v1.17.0 +--- + pgo/pgo.sh | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pgo/pgo.sh b/pgo/pgo.sh -index b2ce7fe..30cb8ec 100755 +index b59f5c21..a65b5aea 100755 --- a/pgo/pgo.sh +++ b/pgo/pgo.sh -@@ -94,7 +94,7 @@ if [ ${do_pgo} = yes ]; then +@@ -95,7 +95,7 @@ if [ ${do_pgo} = yes ]; then -delete meson configure "${blddir}" -Db_pgo=generate @@ -15,10 +20,13 @@ + ninja -C "${blddir}" -v # If fcft/tllist are subprojects, we need to ensure their tests - # have been executed, or we’ll get “profile count data file not -@@ -114,4 +114,4 @@ if [ ${do_pgo} = yes ]; then + # have been executed, or we'll get "profile count data file not +@@ -115,4 +115,4 @@ if [ ${do_pgo} = yes ]; then meson configure "${blddir}" -Db_pgo=use fi -ninja -C "${blddir}" +ninja -C "${blddir}" -v +-- +2.43.0 + diff -Nru foot-1.16.2/debian/watch foot-1.17.2/debian/watch --- foot-1.16.2/debian/watch 2024-01-13 10:11:42.000000000 +0000 +++ foot-1.17.2/debian/watch 2024-04-26 09:55:44.000000000 +0000 @@ -1,2 +1,3 @@ version=4 -https://codeberg.org/dnkl/foot/releases .*/v?(\d\S+)\.tar\.gz +opts="mode=git" \ +https://codeberg.org/dnkl/foot refs/tags/v?(\d\S+) diff -Nru foot-1.16.2/doc/foot-ctlseqs.7.scd foot-1.17.2/doc/foot-ctlseqs.7.scd --- foot-1.16.2/doc/foot-ctlseqs.7.scd 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/doc/foot-ctlseqs.7.scd 2024-04-17 09:26:45.000000000 +0000 @@ -443,13 +443,13 @@ | \\E[ _Pm_ h : SM : VT100 -: Set mode. _Pm_=4 -> enable IRM (insert mode). All other values of - _Pm_ are unsupported. +: Set mode. _Pm_=4 -> enable IRM (Insertion Replacement Mode). All + other values of _Pm_ are unsupported. | \\E[ _Pm_ l : RM : VT100 -: Reset mode. _Pm_=4 -> disable IRM (insert mode). All other values of - _Pm_ are unsupported. +: Reset mode. _Pm_=4 -> disable IRM (Insertion Replacement Mode). All + other values of _Pm_ are unsupported. | \\E[ _Ps_ n : DSR : VT100 @@ -487,7 +487,39 @@ | \\E[ ? _Ps_ $ p : DECRQM : VT320 -: Request DEC private mode. +: Request status of DEC private mode. The _Ps_ parameter corresponds + to one of the values mentioned in the "Private Modes" section above + (as set with DECSET/DECRST). +| \\E[ _Ps_ $ p +: DECRQM +: VT320 +: Request status of ECMA-48/ANSI mode. See the descriptions for SM/RM + above for recognized _Ps_ values. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pm_ $ r +: DECCARA +: VT400 +: Change attributes in rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ + denotes the rectangle, _Pm_ denotes the SGR attributes. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pm_ $ t +: DECRARA +: VT400 +: Invert attributes in rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ + denotes the rectangle, _Pm_ denotes the SGR attributes. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ ; _Pp_ ; _Pt_ ; _Pl_ ; _Pp_ $ v +: DECCRA +: VT400 +: Copy rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ denotes the + rectangle, _Pt_ and _Pl_ denotes the target location. +| \\E[ _Pc_ ; _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ $ x +: DECFRA +: VT420 +: Fill rectangular area. _Pc_ is the character to use, _Pt_, _Pl_, + _Pb_ and _Pr_ denotes the rectangle. +| \\E[ _Pt_ ; _Pl_ ; _Pb_ ; _Pr_ $ z +: DECERA +: VT400 +: Erase rectangular area. _Pt_, _Pl_, _Pb_ and _Pr_ denotes the + rectangle. | \\E[ _Ps_ T : SD : VT420 @@ -687,6 +719,17 @@ | \\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) diff -Nru foot-1.16.2/doc/foot.1.scd foot-1.17.2/doc/foot.1.scd --- foot-1.16.2/doc/foot.1.scd 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/doc/foot.1.scd 2024-04-17 09:26:45.000000000 +0000 @@ -78,6 +78,13 @@ *-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_. @@ -183,16 +190,16 @@ Paste from _clipboard_ *shift*+*insert* - Paste from the _primary selection_. + Paste from the _primary selection_ *ctrl*+*shift*+*r* Start a scrollback search *ctrl*+*+*, *ctrl*+*=* - Increase font size by 0.5pt + Increase font size *ctrl*+*-* - Decrease font size by 0.5pt + Decrease font size *ctrl*+*0* Reset font size @@ -273,6 +280,10 @@ 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* @@ -283,9 +294,16 @@ 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* @@ -313,10 +331,10 @@ 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 +Pressing *ctrl*+*shift*+*o* enters _"Open URL mode"_, where all currently visible URLs are underlined, and is associated with a -_“jump-label”_. The jump-label indicates the _key sequence_ -(e.g. *”AF”*) to use to activate the URL. +_"jump-label"_. The jump-label indicates the _key sequence_ +(e.g. *"AF"*) to use to activate the URL. The key binding can, of course, be customized, like all other key bindings in foot. See *show-urls-launch* and *show-urls-copy* in @@ -398,7 +416,7 @@ New foot terminal instances (bound to *ctrl*+*shift*+*n* by default) will open in the current working directory, if the shell in the -“parent” terminal reports directory changes. +"parent" terminal reports directory changes. This is done with the OSC-7 escape sequence. Most shells can be scripted to do this, if they do not support it natively. See the wiki @@ -424,6 +442,38 @@ (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 @@ -464,10 +514,10 @@ It allows querying the terminal for terminfo classic, file-based, terminfo definition. For example, if all applications used this -feature, you would no longer have to install foot’s terminfo on remote +feature, you would no longer have to install foot's terminfo on remote hosts you SSH into. -XTerm’s implementation (as of XTerm-370) only supports querying key +XTerm's implementation (as of XTerm-370) only supports querying key (as in keyboard keys) capabilities, and three custom capabilities: - TN - terminal name @@ -479,7 +529,7 @@ string capabilities. Foot supports this, and extends it even further, to also include -boolean capabilities. This means foot’s entire terminfo can be queried +boolean capabilities. This means foot's entire terminfo can be queried via *XTGETTCAP*. Note that both Kitty and foot handles responses to multi-capability @@ -490,7 +540,7 @@ - The success/fail flag in the beginning of the response is always 1 (success), unless the very first queried capability is invalid. -- XTerm will not respond at all to an invalid capability, unless it’s +- XTerm will not respond at all to an invalid capability, unless it's the first one in the XTGETTCAP query. - XTerm will end the response at the first invalid capability. @@ -554,16 +604,31 @@ set according to either the *--term* command-line option or the *term* config option in *foot.ini*(5). -*PWD* - Current working directory (at the time of launching foot) - *COLORTERM* This variable is set to *truecolor*, to indicate to client applications that 24-bit RGB colors are supported. +*PWD* + Current working directory (at the time of launching foot) + +*SHELL* + Set to the launched shell, if the shell is valid (it is listed in + */etc/shells*). + In addition to the variables listed above, custom environment variables may be defined in *foot.ini*(5). +## Variables *unset* in the child process + +*TERM_PROGRAM* +*TERM_PROGRAM_VERSION* + These environment variables are set by certain other terminal + emulators. We unset them, to prevent applications from + misdetecting foot. + +In addition to the variables listed above, custom environment +variables to unset may be defined in *foot.ini*(5). + # BUGS Please report bugs to https://codeberg.org/dnkl/foot/issues diff -Nru foot-1.16.2/doc/foot.ini.5.scd foot-1.17.2/doc/foot.ini.5.scd --- foot-1.16.2/doc/foot.ini.5.scd 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/doc/foot.ini.5.scd 2024-04-17 09:26:45.000000000 +0000 @@ -58,6 +58,7 @@ - Dina:weight=bold:slant=italic - Courier New:size=12 - Fantasque Sans Mono:fontfeatures=ss01 + - Meslo LG S:size=12, Noto Color Emoji:size=12 For each option, the first font is the primary font. The remaining fonts are fallback fonts that will be used whenever a glyph cannot @@ -251,6 +252,21 @@ Default: _100_. +*resize-by-cells* + Boolean. + + When set to *yes*, the window size will be constrained to multiples + of the cell size (plus any configured padding). When set to *no*, + the window size will be unconstrained, and padding may be adjusted + as necessary to accommodate window sizes that are not multiples of + the cell size. + + This option only applies to floating windows. Sizes of maxmized, tiled + or fullscreen windows will not be constrained to multiples of the cell + size. + + Default: _yes_ + *initial-window-size-pixels* Initial window width and height in _pixels_ (subject to output scaling), in the form _WIDTHxHEIGHT_. The height _includes_ the @@ -484,6 +500,15 @@ *beam* or *underline*. Note that this can be overridden by applications. Default: _block_. +*unfocused-style* + Configures how the cursor is rendered when the terminal window is + unfocused. Possible values are: + + - unchanged: render cursor in exactly the same way as when the + window has focus. + - hollow: render a block cursor, but hollowed out. + - none: do not display any cursor at all. + *blink* Boolean. Enables blinking cursor. Note that this can be overridden by applications. Default: _no_. @@ -805,11 +830,11 @@ *font-increase* Increases the font size by 0.5pt. Default: _Control+plus - Control+equal Control+KP\_Add_. + Control+equal Control+KP\_Add_ (also defined in *mouse-bindings*). *font-decrease* Decreases the font size by 0.5pt. Default: _Control+minus - Control+KP\_Subtract_. + Control+KP\_Subtract_ (also defined in *mouse-bindings*). *font-reset* Resets the font size to the default. Default: _Control+0 Control+KP\_0_. @@ -828,11 +853,12 @@ *fullscreen* Toggles the fullscreen state. Default: _none_. -*pipe-visible*, *pipe-scrollback*, *pipe-selected* - Pipes the currently visible text, the entire scrollback, or the - currently selected text to an external tool. The syntax for this - option is a bit special; the first part of the value is the - command to execute enclosed in "[]", followed by the binding(s). +*pipe-visible*, *pipe-scrollback*, *pipe-selected*, *pipe-command-output* + Pipes the currently visible text, the entire scrollback, the + currently selected text, or the last command's output to an + external tool. The syntax for this option is a bit special; the + first part of the value is the command to execute enclosed in + "[]", followed by the binding(s). You can configure multiple pipes as long as the command strings are different and the key bindings are unique. @@ -840,9 +866,14 @@ Note that the command is *not* automatically run inside a shell; use *sh -c "command line"* if you need that. - Example: - *pipe-visible=[sh -c "xurls | uniq | tac | fuzzel | xargs -r - firefox"] Control+Print* + Example #1: + # Extract currently visible URLs, let user choose one (via + fuzzel), then launch firefox with the selected URL++ +*pipe-visible=[sh -c "xurls | uniq | tac | fuzzel | xargs -r firefox"] Control+Print* + + 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_ @@ -894,6 +925,9 @@ Default: _Control+Shift+u_. +*quit* + Quit foot. Default: _none_. + # SECTION: search-bindings This section lets you override the default key bindings used in @@ -1206,6 +1240,15 @@ *primary-paste* Pastes from the _primary selection_. Default: _BTN\_MIDDLE_. +*font-increase* + Increases the font size by 0.5pt. Default: _Control+BTN\_BACK_ + (also defined in *key-bindings*). + +*font-decrease* + Decreases the font size by 0.5pt. Default: _Control+BTN\_FORWARD_ + (also defined in *key-bindings*). + + # TWEAK This section is for advanced users and describes configuration options diff -Nru foot-1.16.2/doc/footclient.1.scd foot-1.17.2/doc/footclient.1.scd --- foot-1.16.2/doc/footclient.1.scd 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/doc/footclient.1.scd 2024-04-17 09:26:45.000000000 +0000 @@ -73,6 +73,11 @@ 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_. @@ -163,9 +168,27 @@ This variable is set to *truecolor*, to indicate to client applications that 24-bit RGB colors are supported. +*PWD* + Current working directory (at the time of launching foot) + +*SHELL* + Set to the launched shell, if the shell is valid (it is listed in + */etc/shells*). + In addition to the variables listed above, custom environment variables may be defined in *foot.ini*(5). +## Variables *unset* in the child process + +*TERM_PROGRAM* +*TERM_PROGRAM_VERSION* + These environment variables are set by certain other terminal + emulators. We unset them, to prevent applications from + misdetecting foot. + +In addition to the variables listed above, custom environment +variables to unset may be defined in *foot.ini*(5). + # SEE ALSO *foot*(1) diff -Nru foot-1.16.2/foot.info foot-1.17.2/foot.info --- foot-1.16.2/foot.info 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/foot.info 2024-04-17 09:26:45.000000000 +0000 @@ -38,11 +38,12 @@ 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, Ss=\E[%p1%d q, - Sync=\E[?2026%?%p1%{1}%-%tl%eh, + Sync=\E[?2026%?%p1%{1}%-%tl%eh%;, TS=\E]2;, - XM=\E[?1006;1004;1000%?%p1%{1}%=%th%el%;, + XM=\E[?1006;1000%?%p1%{1}%=%th%el%;, XR=\E[>0q, acsc=``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~, bel=^G, @@ -74,6 +75,8 @@ ed=\E[J, el1=\E[1K, el=\E[K, + fd=\E[?1004l, + fe=\E[?1004h, flash=\E]555\E\\, fsl=\E\\, home=\E[H, @@ -228,6 +231,7 @@ kri=\E[1;2A, kxIN=\E[I, kxOUT=\E[O, + nel=\EE, oc=\E]104\E\\, op=\E[39;49m, rc=\E8, @@ -241,12 +245,13 @@ rmcup=\E[?1049l\E[23;0;0t, rmir=\E[4l, rmkx=\E[?1l\E>, + rmm=\E[?1036h\E[?1034l, rmso=\E[27m, rmul=\E[24m, rmxx=\E[29m, rs1=\Ec, rs2=\E[!p\E[4l\E>, - rv=\E\\[[0-9]+;[0-9]+;[0-9]+c, + rv=\E\\[>1;[0-9][0-9][0-9][0-9][0-9][0-9];0c, sc=\E7, setrgbb=\E[48\:2\:\:%p1%d\:%p2%d\:%p3%dm, setrgbf=\E[38\:2\:\:%p1%d\:%p2%d\:%p3%dm, @@ -258,6 +263,7 @@ smcup=\E[?1049h\E[22;0;0t, smir=\E[4h, smkx=\E[?1h\E=, + smm=\E[?1036l\E[?1034h, smso=\E[7m, smul=\E[4m, smxx=\E[9m, @@ -269,7 +275,7 @@ u9=\E[c, vpa=\E[%i%p1%dd, xm=\E[<%i%p3%d;%p1%d;%p2%d;%?%p4%tM%em%;, - xr=\EP>\\|[ -~]+\E\\\\, + xr=\EP>\\|foot\\([0-9]+\\.[0-9]+\\.[0-9]+(-[0-9]+-g[a-f[0-9]+)?\\)?\E\\\\, # XT, # AX, diff -Nru foot-1.16.2/foot.ini foot-1.17.2/foot.ini --- foot-1.16.2/foot.ini 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/foot.ini 2024-04-17 09:26:45.000000000 +0000 @@ -158,6 +158,7 @@ # 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 @@ -192,6 +193,7 @@ # clipboard-paste=Control+v Control+Shift+v Control+y XF86Paste # primary-paste=Shift+Insert # unicode-input=none +# quit=none # scrollback-up-page=Shift+Page_Up # scrollback-up-half-page=none # scrollback-up-line=none diff -Nru foot-1.16.2/grid.c foot-1.17.2/grid.c --- foot-1.16.2/grid.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/grid.c 2024-04-17 09:26:45.000000000 +0000 @@ -1,5 +1,6 @@ #include "grid.h" +#include #include #include @@ -16,7 +17,7 @@ #define TIME_REFLOW 0 /* - * “sb” (scrollback relative) coordinates + * "sb" (scrollback relative) coordinates * * The scrollback relative row number 0 is the *first*, and *oldest* * row in the scrollback history (and thus the *first* row to be @@ -231,7 +232,7 @@ clone_row->cells = xmalloc(grid->num_cols * sizeof(clone_row->cells[0])); clone_row->linebreak = row->linebreak; clone_row->dirty = row->dirty; - clone_row->prompt_marker = row->prompt_marker; + clone_row->shell_integration = row->shell_integration; for (int c = 0; c < grid->num_cols; c++) clone_row->cells[c] = row->cells[c]; @@ -262,8 +263,7 @@ 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 = xmalloc(original_size); - memcpy(new_original_data, it->item.original.data, original_size); + 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, @@ -283,8 +283,7 @@ int scaled_stride = stride_for_format_and_width(scaled_pix_fmt, scaled_width); size_t scaled_size = scaled_stride * scaled_height; - new_scaled_data = xmalloc(scaled_size); - memcpy(new_scaled_data, it->item.scaled.data, scaled_size); + 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, @@ -366,7 +365,9 @@ row->dirty = false; row->linebreak = false; row->extra = NULL; - row->prompt_marker = false; + row->shell_integration.prompt_marker = false; + row->shell_integration.cmd_start = -1; + row->shell_integration.cmd_end = -1; if (initialize) { row->cells = xcalloc(cols, sizeof(row->cells[0])); @@ -425,7 +426,9 @@ new_row->dirty = old_row->dirty; new_row->linebreak = false; - new_row->prompt_marker = old_row->prompt_marker; + new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker; + new_row->shell_integration.cmd_start = min(old_row->shell_integration.cmd_start, new_cols - 1); + new_row->shell_integration.cmd_end = min(old_row->shell_integration.cmd_end, new_cols - 1); if (new_cols > old_cols) { /* Clear "new" columns */ @@ -587,7 +590,9 @@ /* Scrollback is full, need to reuse a row */ grid_row_reset_extra(new_row); new_row->linebreak = false; - new_row->prompt_marker = false; + new_row->shell_integration.prompt_marker = false; + new_row->shell_integration.cmd_start = -1; + new_row->shell_integration.cmd_end = -1; tll_foreach(old_grid->sixel_images, it) { if (it->item.pos.row == *row_idx) { @@ -599,7 +604,7 @@ /* * TODO: detect if the reused row is covered by the * selection. Of so, cancel the selection. The problem: we - * don’t know if we’ve translated the selection coordinates + * don't know if we've translated the selection coordinates * yet. */ } @@ -609,7 +614,7 @@ return new_row; /* - * URI ranges are per row. Thus, we need to ‘close’ the still-open + * URI ranges are per row. Thus, we need to 'close' the still-open * ranges on the previous row, and re-open them on the * next/current row. */ @@ -789,7 +794,7 @@ } if (!old_row->linebreak && col_count > 0) { - /* Don’t truncate logical lines */ + /* Don't truncate logical lines */ col_count = old_cols; } @@ -831,35 +836,26 @@ int end; bool tp_break = false; bool uri_break = false; + bool ftcs_break = false; - /* - * Set end-coordinate for this chunk, by finding the next - * point-of-interest on this row. - * - * If there are no more tracking points, or URI ranges, - * the end-coordinate will be at the end of the row, - */ - if (range != range_terminator) { - int uri_col = (range->start >= start ? range->start : range->end) + 1; - - if (tp != NULL) { - int tp_col = tp->col + 1; - end = min(tp_col, uri_col); - - tp_break = end == tp_col; - uri_break = end == uri_col; - LOG_DBG("tp+uri break at %d (%d, %d)", end, tp_col, uri_col); - } else { - end = uri_col; - uri_break = true; - LOG_DBG("uri break at %d", end); - } - } else if (tp != NULL) { - end = tp->col + 1; - tp_break = true; - LOG_DBG("TP break at %d", end); - } else - end = col_count; + /* Figure out where to end this chunk */ + { + const int uri_col = range != range_terminator + ? ((range->start >= start ? range->start : range->end) + 1) + : INT_MAX; + const int tp_col = tp != NULL ? tp->col + 1 : INT_MAX; + const int ftcs_col = old_row->shell_integration.cmd_start >= start + ? old_row->shell_integration.cmd_start + 1 + : old_row->shell_integration.cmd_end >= start + ? old_row->shell_integration.cmd_end + 1 + : INT_MAX; + + end = min(col_count, min(min(tp_col, uri_col), ftcs_col)); + + uri_break = end == uri_col; + tp_break = end == tp_col; + ftcs_break = end == ftcs_col; + } int cols = end - start; xassert(cols > 0); @@ -887,8 +883,8 @@ xassert(amount > 0); /* - * If we’re going to reach the end of the new row, we - * need to make sure we don’t end in the middle of a + * If we're going to reach the end of the new row, we + * need to make sure we don't end in the middle of a * multi-column character. */ int spacers = 0; @@ -897,7 +893,7 @@ * While the cell *after* the last cell is a CELL_SPACER * * This means we have a multi-column character - * that doesn’t fit on the current row. We need to + * that doesn't fit on the current row. We need to * push it to the next row, and insert CELL_SPACER * cells as padding. */ @@ -920,7 +916,7 @@ xassert(from + amount <= old_cols); if (from == 0) - new_row->prompt_marker = old_row->prompt_marker; + new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker; memcpy( &new_row->cells[new_col_idx], &old_row->cells[from], @@ -979,6 +975,16 @@ } } + if (ftcs_break) { + xassert(old_row->shell_integration.cmd_start == start + cols - 1 || + old_row->shell_integration.cmd_end == start + cols - 1); + + if (old_row->shell_integration.cmd_start == start + cols - 1) + new_row->shell_integration.cmd_start = new_col_idx - 1; + if (old_row->shell_integration.cmd_end == start + cols - 1) + new_row->shell_integration.cmd_end = new_col_idx - 1; + } + left -= cols; start += cols; } @@ -996,9 +1002,9 @@ { /* * line_wrap() "closes" still-open URIs. Since this is - * the *last* row, and since we’re line-breaking due + * 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 + * 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). @@ -1025,7 +1031,7 @@ xassert(old_rows == 0 || *next_tp == &terminator); #if defined(_DEBUG) - /* Verify all URI ranges have been “closed” */ + /* Verify all URI ranges have been "closed" */ for (int r = 0; r < new_rows; r++) { const struct row *row = new_grid[r]; @@ -1069,7 +1075,7 @@ grid->num_cols = new_cols; /* - * Set new viewport, making sure it’s not too far down. + * Set new viewport, making sure it's not too far down. * * This is done by using scrollback-start relative cooardinates, * and bounding the new viewport to (grid_rows - screen_rows). @@ -1135,7 +1141,7 @@ const bool matching_id = r->id == id; if (matching_id && r->end + 1 == col) { - /* Extend existing URI’s tail */ + /* Extend existing URI's tail */ r->end++; goto out; } @@ -1174,7 +1180,7 @@ uri_range_insert(extra, i + 1, col + 1, r->end, r->id, r->uri); /* The insertion may xrealloc() the vector, making our - * ‘old’ pointer invalid */ + * 'old' pointer invalid */ r = &extra->uri_ranges.v[i]; r->end = col - 1; xassert(r->start <= r->end); @@ -1311,7 +1317,7 @@ extra, i + 1, end + 1, old->end, old->id, old->uri); /* The insertion may xrealloc() the vector, making our - * ‘old’ pointer invalid */ + * 'old' pointer invalid */ old = &extra->uri_ranges.v[i]; old->end = start - 1; return; /* There can be no more URIs affected by the erase range */ @@ -1394,11 +1400,11 @@ * The insertion logic typically triggers an xrealloc(), which, in * some cases, *moves* the entire URI vector to a new base * address. grid_row_uri_range_erase() did not account for this, - * and tried to update the ‘end’ member in the URI range we just + * and tried to update the 'end' member in the URI range we just * split. This causes foot to crash when the xrealloc() has moved * the URI range vector. * - * (note: we’re only verifying we don’t crash here, hence the lack + * (note: we're only verifying we don't crash here, hence the lack * of assertions). */ free(row_data.uri_ranges.v); diff -Nru foot-1.16.2/grid.h foot-1.17.2/grid.h --- foot-1.16.2/grid.h 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/grid.h 2024-04-17 09:26:45.000000000 +0000 @@ -86,7 +86,6 @@ void grid_row_uri_range_put( struct row *row, int col, const char *uri, uint64_t id); -void grid_row_uri_range_add(struct row *row, struct row_uri_range range); void grid_row_uri_range_erase(struct row *row, int start, int end); static inline void diff -Nru foot-1.16.2/input.c foot-1.17.2/input.c --- foot-1.16.2/input.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/input.c 2024-04-17 09:26:45.000000000 +0000 @@ -227,7 +227,8 @@ break; /* FALLTHROUGH */ case BIND_ACTION_PIPE_VIEW: - case BIND_ACTION_PIPE_SELECTED: { + case BIND_ACTION_PIPE_SELECTED: + case BIND_ACTION_PIPE_COMMAND_OUTPUT: { if (binding->aux->type != BINDING_AUX_PIPE) return true; @@ -269,6 +270,10 @@ 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; @@ -377,7 +382,7 @@ const struct row *row = grid->rows[r_abs]; xassert(row != NULL); - if (!row->prompt_marker) + if (!row->shell_integration.prompt_marker) continue; grid->view = r_abs; @@ -409,9 +414,9 @@ const struct row *row = grid->rows[r_abs]; xassert(row != NULL); - if (!row->prompt_marker) { + if (!row->shell_integration.prompt_marker) { if (r_abs == grid->offset + term->rows - 1) { - /* We’ve reached the bottom of the scrollback */ + /* We've reached the bottom of the scrollback */ break; } continue; @@ -429,7 +434,7 @@ term_damage_view(term); render_refresh(term); - break; + break; } return true; @@ -439,6 +444,10 @@ unicode_mode_activate(seat); return true; + case BIND_ACTION_QUIT: + term_shutdown(term); + return true; + case BIND_ACTION_SELECT_BEGIN: selection_start( term, seat->mouse.col, seat->mouse.row, SELECTION_CHAR_WISE, false); @@ -579,17 +588,19 @@ seat->kbd.mod_caps = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_CAPS); seat->kbd.mod_num = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, XKB_MOD_NAME_NUM); - seat->kbd.bind_significant = 0; + /* Significant modifiers in the legacy keyboard protocol */ + seat->kbd.legacy_significant = 0; if (seat->kbd.mod_shift != XKB_MOD_INVALID) - seat->kbd.bind_significant |= 1 << seat->kbd.mod_shift; + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_shift; if (seat->kbd.mod_alt != XKB_MOD_INVALID) - seat->kbd.bind_significant |= 1 << seat->kbd.mod_alt; + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_alt; if (seat->kbd.mod_ctrl != XKB_MOD_INVALID) - seat->kbd.bind_significant |= 1 << seat->kbd.mod_ctrl; + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_ctrl; if (seat->kbd.mod_super != XKB_MOD_INVALID) - seat->kbd.bind_significant |= 1 << seat->kbd.mod_super; + seat->kbd.legacy_significant |= 1 << seat->kbd.mod_super; - seat->kbd.kitty_significant = seat->kbd.bind_significant; + /* Significant modifiers in the kitty keyboard protocol */ + seat->kbd.kitty_significant = seat->kbd.legacy_significant; if (seat->kbd.mod_caps != XKB_MOD_INVALID) seat->kbd.kitty_significant |= 1 << seat->kbd.mod_caps; if (seat->kbd.mod_num != XKB_MOD_INVALID) @@ -854,7 +865,7 @@ const struct key_data *info = keymap_lookup(&term, XKB_KEY_ISO_Left_Tab, MOD_SHIFT | MOD_CTRL); xassert(info != NULL); - xassert(strcmp(info->seq, "\033[27;6;9~") == 0); + xassert(streq(info->seq, "\033[27;6;9~")); } UNITTEST @@ -865,18 +876,19 @@ const struct key_data *info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT); xassert(info != NULL); - xassert(strcmp(info->seq, "\033\r") == 0); + xassert(streq(info->seq, "\033\r")); term.modify_other_keys_2 = true; info = keymap_lookup(&term, XKB_KEY_Return, MOD_ALT); xassert(info != NULL); - xassert(strcmp(info->seq, "\033[27;3;13~") == 0); + xassert(streq(info->seq, "\033[27;3;13~")); } void get_current_modifiers(const struct seat *seat, xkb_mod_mask_t *effective, - xkb_mod_mask_t *consumed, uint32_t key) + xkb_mod_mask_t *consumed, uint32_t key, + bool filter_locked) { if (unlikely(seat->kbd.xkb_state == NULL)) { if (effective != NULL) @@ -886,24 +898,27 @@ } else { + const xkb_mod_mask_t locked = + xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + if (effective != NULL) { *effective = xkb_state_serialize_mods( seat->kbd.xkb_state, XKB_STATE_MODS_EFFECTIVE); + + if (filter_locked) + *effective &= ~locked; } if (consumed != NULL) { *consumed = xkb_state_key_get_consumed_mods2( seat->kbd.xkb_state, key, XKB_CONSUMED_MODE_XKB); + + if (filter_locked) + *consumed &= ~locked; } } } -static xkb_mod_mask_t -get_locked_modifiers(const struct seat *seat) -{ - return xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); -} - struct kbd_ctx { xkb_layout_index_t layout; xkb_keycode_t key; @@ -958,8 +973,8 @@ #define is_control_key(x) ((x) >= 0x40 && (x) <= 0x7f) #define IS_CTRL(x) ((x) < 0x20 || ((x) >= 0x7f && (x) <= 0x9f)) - LOG_DBG("term->modify_other_keys=%d, count=%zu, is_ctrl=%d (utf8=0x%02x), sym=%d", - term->modify_other_keys_2, count, IS_CTRL(utf8[0]), utf8[0], sym); + //LOG_DBG("term->modify_other_keys=%d, count=%zu, is_ctrl=%d (utf8=0x%02x), sym=%d", + //term->modify_other_keys_2, count, IS_CTRL(utf8[0]), utf8[0], sym); bool ctrl_is_in_effect = (keymap_mods & MOD_CTRL) != 0; bool ctrl_seq = is_control_key(sym) || (count == 1 && IS_CTRL(utf8[0])); @@ -968,24 +983,24 @@ if (term->modify_other_keys_2) { /* - * Try to mimic XTerm’s behavior, when holding shift: + * Try to mimic XTerm's behavior, when holding shift: * * - if other modifiers are pressed (e.g. Alt), emit a CSI escape * - upper-case symbols A-Z are encoded as an CSI escape - * - other upper-case symbols (e.g ‘Ö’) or emitted as is + * - other upper-case symbols (e.g 'Ö') or emitted as is * - non-upper cased symbols are _mostly_ emitted as is (foot * always emits as is) * * Examples (assuming Swedish layout): - * - Shift-a (‘A’) emits a CSI - * - Shift-, (‘;’) emits ‘;’ + * - Shift-a ('A') emits a CSI + * - Shift-, (';') emits ';' * - Shift-Alt-, (Alt-;) emits a CSI - * - Shift-ö (‘Ö’) emits ‘Ö’ + * - Shift-ö ('Ö') emits 'Ö' */ /* Any modifiers, besides shift active? */ const xkb_mod_mask_t shift_mask = 1 << seat->kbd.mod_shift; - if ((ctx->mods & ~shift_mask & seat->kbd.bind_significant) != 0) + if ((ctx->mods & ~shift_mask & seat->kbd.legacy_significant) != 0) modify_other_keys2_in_effect = true; else { @@ -993,9 +1008,9 @@ seat->kbd.xkb_state, ctx->key); /* - * Get pressed key’s base symbol. - * - for ‘A’ (shift-a), that’s ‘a’ - * - for ‘;’ (shift-,), that’s ‘,’ + * Get pressed key's base symbol. + * - for 'A' (shift-a), that's 'a' + * - for ';' (shift-,), that's ',' */ const xkb_keysym_t *base_syms = NULL; size_t base_count = xkb_keymap_key_get_syms_by_level( @@ -1131,24 +1146,96 @@ if (composed && released) return false; - /* TODO: should we even bother with this, or just say it’s not supported? */ + /* TODO: should we even bother with this, or just say it's not supported? */ if (!disambiguate && !report_all_as_escapes && pressed) return legacy_kbd_protocol(seat, term, ctx); - const xkb_mod_mask_t mods = ctx->mods & seat->kbd.kitty_significant; - const xkb_mod_mask_t consumed = xkb_state_key_get_consumed_mods2( - seat->kbd.xkb_state, ctx->key, XKB_CONSUMED_MODE_GTK) & seat->kbd.kitty_significant; - const xkb_mod_mask_t effective = mods & ~consumed; - const xkb_mod_mask_t caps_num = - (seat->kbd.mod_caps != XKB_MOD_INVALID ? 1 << seat->kbd.mod_caps : 0) | - (seat->kbd.mod_num != XKB_MOD_INVALID ? 1 << seat->kbd.mod_num : 0); - const xkb_keysym_t sym = ctx->sym; const uint32_t *utf32 = ctx->utf32; const uint8_t *const utf8 = ctx->utf8.buf; const size_t count = ctx->utf8.count; - bool is_text = count > 0 && utf32 != NULL && (effective & ~caps_num) == 0; + /* Lookup sym in the pre-defined keysym table */ + const struct kitty_key_data *info = bsearch( + &sym, kitty_keymap, ALEN(kitty_keymap), sizeof(kitty_keymap[0]), + &kitty_search); + xassert(info == NULL || info->sym == sym); + + xkb_mod_mask_t mods = 0; + xkb_mod_mask_t locked = 0; + xkb_mod_mask_t consumed = ctx->consumed; + + if (info != NULL && info->is_modifier) { + /* + * Special-case modifier keys. + * + * Normally, the "current" XKB state reflects the state + * *before* the current key event. In other words, the + * modifiers for key events that affect the modifier state + * (e.g. one of the control keys, or shift keys etc) does + * *not* include the key itself. + * + * Put another way, if you press "control", the modifier set + * is empty in the key press event, but contains "ctrl" in the + * release event. + * + * The kitty protocol mandates the modifier list contain the + * key itself, in *both* the press and release event. + * + * We handle this by updating the XKB state to *include* the + * current key, retrieve the set of modifiers (including the + * set of consumed modifiers), and then revert the XKB update. + */ + xkb_state_update_key( + seat->kbd.xkb_state, ctx->key, pressed ? XKB_KEY_DOWN : XKB_KEY_UP); + + get_current_modifiers(seat, &mods, NULL, 0, false); + + locked = xkb_state_serialize_mods( + seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + consumed = xkb_state_key_get_consumed_mods2( + seat->kbd.xkb_state, ctx->key, XKB_CONSUMED_MODE_XKB); + +#if 0 + /* + * TODO: according to the XKB docs, state updates should + * always be in pairs: each press should be followed by a + * release. However, doing this just breaks the xkb state. + * + * *Not* pairing the above press/release with a corresponding + * release/press appears to do exactly what we want. + */ + xkb_state_update_key( + seat->kbd.xkb_state, ctx->key, pressed ? XKB_KEY_UP : XKB_KEY_DOWN); +#endif + } else { + /* Same as ctx->mods, but *without* filtering locked modifiers */ + get_current_modifiers(seat, &mods, NULL, 0, false); + locked = xkb_state_serialize_mods( + seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + } + + mods &= seat->kbd.kitty_significant; + consumed &= seat->kbd.kitty_significant; + + /* + * A note on locked modifiers; they *are* a part of the protocol, + * and *should* be included in the modifier set reported in the + * key event. + * + * However, *only* if the key would result in a CSIu *without* the + * locked modifier being enabled + * + * Translated: if *another* modifier is active, or if + * report-all-keys-as-escapes is enabled, then we include the + * locked modifier in the key event. + * + * But, if the key event would result in plain text output without + * the locked modifier, then we "ignore" the locked modifier and + * emit plain text anyway. + */ + + bool is_text = count > 0 && utf32 != NULL && (mods & ~locked & ~consumed) == 0; for (size_t i = 0; utf32[i] != U'\0'; i++) { if (!iswprint(utf32[i])) { is_text = false; @@ -1159,12 +1246,6 @@ const bool report_associated_text = (flags & KITTY_KBD_REPORT_ASSOCIATED) && is_text && !released; - /* Lookup sym in the pre-defined keysym table */ - const struct kitty_key_data *info = bsearch( - &sym, kitty_keymap, ALEN(kitty_keymap), sizeof(kitty_keymap[0]), - &kitty_search); - xassert(info == NULL || info->sym == sym); - if (composing) { /* We never emit anything while composing, *except* modifiers * (and only in report-all-keys-as-escape-codes mode) */ @@ -1177,7 +1258,7 @@ if (report_all_as_escapes) goto emit_escapes; - if (effective == 0) { + if ((mods & ~locked & ~consumed) == 0) { switch (sym) { case XKB_KEY_Return: term_to_slave(term, "\r", 1); return true; case XKB_KEY_BackSpace: term_to_slave(term, "\x7f", 1); return true; @@ -1222,32 +1303,32 @@ * * 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’). + * both use the same value for 'key' (97 - i.a. 'a'). * - * However, don’t do this if a non-significant modifier was + * 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” + * 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 + * 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 ‘²’ + * 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 + * The kitty kbd protocol can't encode AltGr. So, if we + * always used the base symbol ('2'), Alt+Shift+2 would * result in the same escape sequence as * AltGr+Alt+Shift+2. * * (yes, this matches what kitty does, as of 0.23.1) */ - /* Get the key’s shift level */ + /* Get the key's shift level */ xkb_level_index_t lvl = xkb_state_key_get_level( seat->kbd.xkb_state, ctx->key, ctx->layout); @@ -1259,7 +1340,7 @@ masks, ALEN(masks)); /* Check modifier combinations - if a combination has - * modifiers not in our set of ‘significant’ modifiers, + * modifiers not in our set of 'significant' modifiers, * use key sym as-is */ bool use_level0_sym = true; for (size_t i = 0; i < mask_count; i++) { @@ -1306,7 +1387,7 @@ char event[4]; if (report_events /*&& !pressed*/) { /* Note: this deviates slightly from Kitty, which omits the - * “:1” subparameter for key press events */ + * ":1" subparameter for key press events */ event[0] = ':'; event[1] = '0' + (pressed ? 1 : repeating ? 2 : 3); event[2] = '\0'; @@ -1384,6 +1465,34 @@ 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) @@ -1427,13 +1536,7 @@ const bool composed = compose_status == XKB_COMPOSE_COMPOSED; xkb_mod_mask_t mods, consumed; - get_current_modifiers(seat, &mods, &consumed, key); - - const xkb_mod_mask_t locked = get_locked_modifiers(seat); - const xkb_mod_mask_t bind_mods - = mods & seat->kbd.bind_significant & ~locked; - const xkb_mod_mask_t bind_consumed = - consumed & seat->kbd.bind_significant & ~locked; + get_current_modifiers(seat, &mods, &consumed, key, true); xkb_layout_index_t layout_idx = xkb_state_key_get_layout(seat->kbd.xkb_state, key); @@ -1457,7 +1560,7 @@ start_repeater(seat, key); search_input( - seat, term, bindings, key, sym, mods, consumed, locked, + seat, term, bindings, key, sym, mods, consumed, raw_syms, raw_count, serial); return; } @@ -1467,28 +1570,34 @@ start_repeater(seat, key); urls_input( - seat, term, bindings, key, sym, mods, consumed, locked, + seat, term, bindings, key, sym, mods, consumed, raw_syms, raw_count, serial); return; } } -#if 0 - for (size_t i = 0; i < 32; i++) { - if (mods & (1 << i)) { - LOG_INFO("%s", xkb_keymap_mod_get_name(seat->kbd.xkb_keymap, i)); - } - } -#endif - #if defined(_DEBUG) && defined(LOG_ENABLE_DBG) && LOG_ENABLE_DBG char sym_name[100]; xkb_keysym_get_name(sym, sym_name, sizeof(sym_name)); - LOG_DBG("%s (%u/0x%x): seat=%s, term=%p, serial=%u, " - "mods=0x%08x, consumed=0x%08x, repeats=%d", - sym_name, sym, sym, seat->name, (void *)term, serial, - mods, consumed, should_repeat); + char active_mods_str[256] = {0}; + char consumed_mods_str[256] = {0}; + char locked_mods_str[256] = {0}; + + const xkb_mod_mask_t locked = + xkb_state_serialize_mods(seat->kbd.xkb_state, XKB_STATE_MODS_LOCKED); + + modifier_string(mods, sizeof(active_mods_str), active_mods_str, seat); + modifier_string(consumed, sizeof(consumed_mods_str), consumed_mods_str, seat); + modifier_string(locked, sizeof(locked_mods_str), locked_mods_str, seat); + + LOG_DBG("%s: %s (%u/0x%x), seat=%s, term=%p, serial=%u, " + "mods=%s (0x%08x), consumed=%s (0x%08x), locked=%s (0x%08x), " + "repeats=%d", + pressed ? "pressed" : "released", sym_name, sym, sym, + seat->name, (void *)term, serial, + active_mods_str, mods, consumed_mods_str, consumed, + locked_mods_str, locked, should_repeat); #endif /* @@ -1500,13 +1609,13 @@ /* Match translated symbol */ if (bind->k.sym == sym && - bind->mods == (bind_mods & ~bind_consumed) && + bind->mods == (mods & ~consumed) && execute_binding(seat, term, bind, serial, 1)) { goto maybe_repeat; } - if (bind->mods != bind_mods || bind_mods != (mods & ~locked)) + if (bind->mods != mods) continue; /* Match untranslated symbols */ @@ -1596,7 +1705,7 @@ if (utf8 != buf) free(utf8); - if (handled) { + if (handled && !keysym_is_modifier(sym)) { term_reset_view(term); selection_cancel(term); } @@ -1626,8 +1735,21 @@ { struct seat *seat = data; - LOG_DBG("modifiers: depressed=0x%x, latched=0x%x, locked=0x%x, group=%u", - mods_depressed, mods_latched, mods_locked, group); +#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( @@ -1970,7 +2092,7 @@ * event with a NULL surface - see wl_pointer_enter(). * * In this case, we never set seat->mouse_focus (since we - * can’t map the enter event to a specific window). */ + * can't map the enter event to a specific window). */ return; } @@ -2079,7 +2201,7 @@ if (cursor_is_on_new_cell) { /* Prevent multiple/different mouse bindings from - * triggering if the mouse has moved “too much” (to + * triggering if the mouse has moved "too much" (to * another cell) */ seat->mouse.count = 0; } @@ -2099,14 +2221,14 @@ if (!term->is_searching) { if (auto_scroll_direction != SELECTION_SCROLL_NOT) { /* - * Start ‘selection auto-scrolling’ + * Start 'selection auto-scrolling' * * The speed of the scrolling is proportional to the * distance between the mouse and the grid; the * further away the mouse is, the faster we scroll. * - * Note that the speed is measured in ‘intervals (in - * ns) between each timed scroll of a single line’. + * Note that the speed is measured in 'intervals (in + * ns) between each timed scroll of a single line'. * * Thus, the further away the mouse is, the smaller * interval value we use. @@ -2187,8 +2309,7 @@ xassert(bindings != NULL); xkb_mod_mask_t mods; - get_current_modifiers(seat, &mods, NULL, 0); - mods &= seat->kbd.bind_significant; + get_current_modifiers(seat, &mods, NULL, 0, true); /* Ignore selection override modifiers when * matching modifiers */ @@ -2241,8 +2362,7 @@ continue; } - const struct config_key_modifiers no_mods = {0}; - if (memcmp(&binding->modifiers, &no_mods, sizeof(no_mods)) != 0) { + if (tll_length(binding->modifiers) > 0) { /* Binding has modifiers */ continue; } @@ -2315,7 +2435,7 @@ * clicking twice, waiting for the CSD timer, and finally * clicking once more, results in the following sequence * (keyboard and other irrelevant events filtered out, unless - * they’re needed to prove a point): + * they're needed to prove a point): * * dbg: input.c:1551: cancelling drag timer, moving window * dbg: input.c:759: keyboard_leave: keyboard=0x607000003580, serial=873, surface=0x6070000036d0 @@ -2347,12 +2467,12 @@ * - GNOME does *not* send a pointer *enter* event after the drag * has stopped * - The second drag does *not* generate a pointer *leave* event - * - The missing leave event means we’re still tracking LMB as + * - The missing leave event means we're still tracking LMB as * being held down in our seat struct. * - This leads to an assert (debug builds) when LMB is clicked - * again (seat’s button list already contains LMB). + * again (seat's button list already contains LMB). * - * Note: I’ve also observed variants of the above + * Note: I've also observed variants of the above */ tll_foreach(seat->mouse.buttons, it) { if (it->item.button == button) { diff -Nru foot-1.16.2/input.h foot-1.17.2/input.h --- foot-1.16.2/input.h 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/input.h 2024-04-17 09:26:45.000000000 +0000 @@ -11,12 +11,12 @@ * Custom defines for mouse wheel left/right buttons. * * Libinput does not define these. On Wayland, all scroll events (both - * vertical and horizontal) are reported not as buttons, as ‘axis’ + * vertical and horizontal) are reported not as buttons, as 'axis' * events. * * Libinput _does_ define BTN_BACK and BTN_FORWARD, which is * what we use for vertical scroll events. But for horizontal scroll - * events, there aren’t any pre-defined mouse buttons. + * events, there aren't any pre-defined mouse buttons. * * Mouse buttons are in the range 0x110 - 0x11f, with joystick defines * starting at 0x120. @@ -33,6 +33,6 @@ void get_current_modifiers(const struct seat *seat, xkb_mod_mask_t *effective, xkb_mod_mask_t *consumed, - uint32_t key); + uint32_t key, bool filter_locked); enum cursor_shape xcursor_for_csd_border(struct terminal *term, int x, int y); diff -Nru foot-1.16.2/key-binding.c foot-1.17.2/key-binding.c --- foot-1.16.2/key-binding.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/key-binding.c 2024-04-17 09:26:45.000000000 +0000 @@ -243,27 +243,27 @@ * modifier, and replace the shifted symbol with its unshifted * variant. * - * For example, the combo is “Control+Shift+U”. In this case, - * Shift is the modifier used to “shift” ‘u’ to ‘U’, after which - * ‘Shift’ will have been “consumed”. Since we filter out consumed + * For example, the combo is "Control+Shift+U". In this case, + * Shift is the modifier used to "shift" 'u' to 'U', after which + * 'Shift' will have been "consumed". Since we filter out consumed * modifiers when matching key combos, this key combo will never - * trigger (we will never be able to match the ‘Shift’ modifier). + * trigger (we will never be able to match the 'Shift' modifier). * * There are two correct variants of the above key combo: - * - “Control+U” (upper case ‘U’) - * - “Control+Shift+u” (lower case ‘u’) + * - "Control+U" (upper case 'U') + * - "Control+Shift+u" (lower case 'u') * * What we do here is, for each key *code*, check if there are any - * (shifted) levels where it produces ‘sym’. If there are, check + * (shifted) levels where it produces 'sym'. If there are, check * *which* sets of modifiers are needed to produce it, and compare - * with ‘mods’. + * with 'mods'. * - * If there is at least one common modifier, it means ‘sym’ is a - * “shifted” symbol, with the corresponding shifting modifier + * If there is at least one common modifier, it means 'sym' is a + * "shifted" symbol, with the corresponding shifting modifier * explicitly included in the key combo. I.e. the key combo will * never trigger. * - * We then proceed and “repair” the key combo by replacing ‘sym’ + * We then proceed and "repair" the key combo by replacing 'sym' * with the corresponding unshifted symbol. * * To reduce the noise, we ignore all key codes where the shifted @@ -283,7 +283,7 @@ seat->kbd.xkb_keymap, code, layout_idx, 0, &base_syms); if (base_count == 0 || sym == base_syms[0]) { - /* No unshifted symbols, or unshifted symbol is same as ‘sym’ */ + /* No unshifted symbols, or unshifted symbol is same as 'sym' */ continue; } @@ -313,7 +313,7 @@ seat->kbd.xkb_keymap, code, layout_idx, level_idx, mod_masks, ALEN(mod_masks)); - /* Check if key combo’s modifier set intersects */ + /* Check if key combo's modifier set intersects */ for (size_t j = 0; j < mod_mask_count; j++) { if ((mod_masks[j] & mods) != mod_masks[j]) continue; @@ -359,19 +359,19 @@ * Sort bindings such that bindings with the same symbol are * sorted with the binding having the most modifiers comes first. * - * This fixes an issue where the “wrong” key binding are triggered - * when used with “consumed” modifiers. + * This fixes an issue where the "wrong" key binding are triggered + * when used with "consumed" modifiers. * * For example: if Control+BackSpace is bound before * Control+Shift+BackSpace, then the latter binding is never * triggered. * * Why? Because Shift is a consumed modifier. This means - * Control+BackSpace is “the same” as Control+Shift+BackSpace. + * Control+BackSpace is "the same" as Control+Shift+BackSpace. * * By sorting bindings with more modifiers first, we work around - * the problem. But note that it is *just* a workaround, and I’m - * not confident there aren’t cases where it doesn’t work. + * the problem. But note that it is *just* a workaround, and I'm + * not confident there aren't cases where it doesn't work. * * See https://codeberg.org/dnkl/foot/issues/1280 */ @@ -404,6 +404,24 @@ tll_sort(*list, key_cmp); } +static xkb_mod_mask_t +mods_to_mask(const struct seat *seat, const config_modifier_list_t *mods) +{ + xkb_mod_mask_t mask = 0; + tll_foreach(*mods, it) { + xkb_mod_index_t idx = xkb_keymap_mod_get_index(seat->kbd.xkb_keymap, it->item); + + if (idx == XKB_MOD_INVALID) { + LOG_ERR("%s: invalid modifier name", it->item); + continue; + } + + mask |= 1 << idx; + } + + return mask; +} + static void NOINLINE convert_key_binding(struct key_set *set, const struct config_key_binding *conf_binding, @@ -411,7 +429,7 @@ { const struct seat *seat = set->seat; - xkb_mod_mask_t mods = conf_modifiers_to_mask(seat, &conf_binding->modifiers); + xkb_mod_mask_t mods = mods_to_mask(seat, &conf_binding->modifiers); xkb_keysym_t sym = maybe_repair_key_combo(seat, conf_binding->k.sym, mods); struct key_binding binding = { @@ -469,7 +487,7 @@ .type = MOUSE_BINDING, .action = conf_binding->action, .aux = &conf_binding->aux, - .mods = conf_modifiers_to_mask(set->seat, &conf_binding->modifiers), + .mods = mods_to_mask(set->seat, &conf_binding->modifiers), .m = { .button = conf_binding->m.button, .count = conf_binding->m.count, @@ -509,7 +527,7 @@ convert_url_bindings(set); convert_mouse_bindings(set); - set->public.selection_overrides = conf_modifiers_to_mask( + set->public.selection_overrides = mods_to_mask( set->seat, &set->conf->mouse.selection_override_modifiers); } diff -Nru foot-1.16.2/key-binding.h foot-1.17.2/key-binding.h --- foot-1.16.2/key-binding.h 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/key-binding.h 2024-04-17 09:26:45.000000000 +0000 @@ -32,6 +32,7 @@ BIND_ACTION_PIPE_SCROLLBACK, BIND_ACTION_PIPE_VIEW, BIND_ACTION_PIPE_SELECTED, + BIND_ACTION_PIPE_COMMAND_OUTPUT, BIND_ACTION_SHOW_URLS_COPY, BIND_ACTION_SHOW_URLS_LAUNCH, BIND_ACTION_SHOW_URLS_PERSISTENT, @@ -39,6 +40,7 @@ BIND_ACTION_PROMPT_PREV, BIND_ACTION_PROMPT_NEXT, BIND_ACTION_UNICODE_INPUT, + BIND_ACTION_QUIT, /* Mouse specific actions - i.e. they require a mouse coordinate */ BIND_ACTION_SCROLLBACK_UP_MOUSE, @@ -52,7 +54,7 @@ BIND_ACTION_SELECT_QUOTE, BIND_ACTION_SELECT_ROW, - BIND_ACTION_KEY_COUNT = BIND_ACTION_UNICODE_INPUT + 1, + BIND_ACTION_KEY_COUNT = BIND_ACTION_QUIT + 1, BIND_ACTION_COUNT = BIND_ACTION_SELECT_ROW + 1, }; diff -Nru foot-1.16.2/kitty-keymap.h foot-1.17.2/kitty-keymap.h --- foot-1.16.2/kitty-keymap.h 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/kitty-keymap.h 2024-04-17 09:26:45.000000000 +0000 @@ -13,7 +13,7 @@ _Static_assert(sizeof(struct kitty_key_data) == 7, "bad size"); -/* Note! *Must* Be kept sorted (on ‘sym’) */ +/* Note! *Must* Be kept sorted (on 'sym') */ static const struct kitty_key_data kitty_keymap[] = { {XKB_KEY_ISO_Level3_Shift, 57453, 'u', true}, {XKB_KEY_ISO_Level5_Shift, 57454, 'u', true}, diff -Nru foot-1.16.2/log.c foot-1.17.2/log.c --- foot-1.16.2/log.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/log.c 2024-04-17 09:26:45.000000000 +0000 @@ -105,6 +105,9 @@ if (!do_syslog) return; + if (log_class > log_level) + return; + /* Map our log level to syslog's level */ int level = log_level_map[log_class].syslog_equivalent; @@ -199,7 +202,7 @@ return -1; for (int i = 0, n = map_len(); i < n; i++) - if (strcmp(str, log_level_map[i].name) == 0) + if (streq(str, log_level_map[i].name)) return i; return -1; diff -Nru foot-1.16.2/main.c foot-1.17.2/main.c --- foot-1.16.2/main.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/main.c 2024-04-17 09:26:45.000000000 +0000 @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -77,6 +78,7 @@ " -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" @@ -86,7 +88,7 @@ " -p,--print-pid=FILE|FD print PID to file or FD (only applicable in server mode)\n" " -d,--log-level={info|warning|error|none} log level (warning)\n" " -l,--log-colorize=[{never|always|auto}] enable/disable colorization of log output on stderr\n" - " -s,--log-no-syslog disable syslog logging (only applicable in server mode)\n" + " -S,--log-no-syslog disable syslog logging (only applicable in server mode)\n" " -v,--version show the version number and quit\n" " -e ignored (for compatibility with xterm -e)\n"; @@ -172,6 +174,10 @@ sigaction(i, &dfl, NULL); } +enum { + PTY_OPTION = CHAR_MAX + 1, +}; + int main(int argc, char *const *argv) { @@ -209,6 +215,7 @@ {"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'}, @@ -221,6 +228,7 @@ 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; @@ -253,7 +261,7 @@ break; case 't': - tll_push_back(overrides, xasprintf("term=%s", optarg)); + tll_push_back(overrides, xstrjoin("term=", optarg)); break; case 'L': @@ -261,11 +269,11 @@ break; case 'T': - tll_push_back(overrides, xasprintf("title=%s", optarg)); + tll_push_back(overrides, xstrjoin("title=", optarg)); break; case 'a': - tll_push_back(overrides, xasprintf("app-id=%s", optarg)); + tll_push_back(overrides, xstrjoin("app-id=", optarg)); break; case 'D': { @@ -279,7 +287,7 @@ } case 'f': { - char *font_override = xasprintf("font=%s", optarg); + char *font_override = xstrjoin("font=", optarg); tll_push_back(overrides, font_override); break; } @@ -316,6 +324,10 @@ conf_server_socket_path = optarg; break; + case PTY_OPTION: + pty_path = optarg; + break; + case 'P': presentation_timings = true; break; @@ -351,11 +363,11 @@ } case 'l': - if (optarg == NULL || strcmp(optarg, "auto") == 0) + if (optarg == NULL || streq(optarg, "auto")) log_colorize = LOG_COLORIZE_AUTO; - else if (strcmp(optarg, "never") == 0) + else if (streq(optarg, "never")) log_colorize = LOG_COLORIZE_NEVER; - else if (strcmp(optarg, "always") == 0) + else if (streq(optarg, "always")) log_colorize = LOG_COLORIZE_ALWAYS; else { fprintf(stderr, "%s: argument must be one of 'never', 'always' or 'auto'\n", optarg); @@ -383,6 +395,11 @@ } } + 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); @@ -427,7 +444,7 @@ /* * Try to force an UTF-8 locale. If we succeed, launch the - * user’s shell as usual, but add a user-notification saying + * user's shell as usual, but add a user-notification saying * the locale has been changed. */ for (size_t i = 0; i < ALEN(fallback_locales); i++) { @@ -538,7 +555,7 @@ if (resolved_path_cwd != NULL && resolved_path_pwd != NULL && - strcmp(resolved_path_cwd, resolved_path_pwd) == 0) + streq(resolved_path_cwd, resolved_path_pwd)) { /* * The resolved path of $PWD matches the resolved path of @@ -574,7 +591,7 @@ goto out; if (!as_server && (term = term_init( - &conf, fdm, reaper, wayl, "foot", cwd, token, + &conf, fdm, reaper, wayl, "foot", cwd, token, pty_path, argc, argv, NULL, &term_shutdown_cb, &shutdown_ctx)) == NULL) { goto out; diff -Nru foot-1.16.2/meson.build foot-1.17.2/meson.build --- foot-1.16.2/meson.build 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/meson.build 2024-04-17 09:26:45.000000000 +0000 @@ -1,5 +1,5 @@ project('foot', 'c', - version: '1.16.2', + version: '1.17.2', license: 'MIT', meson_version: '>=0.59.0', default_options: [ diff -Nru foot-1.16.2/notify.c foot-1.17.2/notify.c --- foot-1.16.2/notify.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/notify.c 2024-04-17 09:26:45.000000000 +0000 @@ -20,7 +20,7 @@ LOG_DBG("notify: title=\"%s\", msg=\"%s\"", title, body); if (term->conf->notify_focus_inhibit && term->kbd_focus) { - /* No notifications while we’re focused */ + /* No notifications while we're focused */ return; } @@ -36,7 +36,7 @@ if (!spawn_expand_template( &term->conf->notify, 4, (const char *[]){"app-id", "window-title", "title", "body"}, - (const char *[]){term->conf->app_id, term->window_title, title, body}, + (const char *[]){term->app_id ? term->app_id : term->conf->app_id, term->window_title, title, body}, &argc, &argv)) { return; diff -Nru foot-1.16.2/org.codeberg.dnkl.foot.metainfo.xml foot-1.17.2/org.codeberg.dnkl.foot.metainfo.xml --- foot-1.16.2/org.codeberg.dnkl.foot.metainfo.xml 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/org.codeberg.dnkl.foot.metainfo.xml 2024-04-17 09:26:45.000000000 +0000 @@ -33,10 +33,16 @@ - - - - + + + + + + + + + + org.codeberg.dnkl.foot.desktop https://codeberg.org/dnkl/foot diff -Nru foot-1.16.2/osc.c foot-1.17.2/osc.c --- foot-1.16.2/osc.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/osc.c 2024-04-17 09:26:45.000000000 +0000 @@ -353,7 +353,7 @@ return false; } - /* Verify prefix is “rgb:” or “rgba:” */ + /* Verify prefix is "rgb:" or "rgba:" */ if (have_alpha) { if (strncmp(string, "rgba:", 5) != 0) return false; @@ -426,7 +426,7 @@ return; } - if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { + if (streq(scheme, "file") && hostname_is_localhost(host)) { LOG_DBG("OSC7: pwd: %s", path); free(term->cwd); term->cwd = path; @@ -443,9 +443,9 @@ /* * \E]8;;URI\e\\ * - * Params are key=value pairs, separated by ‘:’. + * Params are key=value pairs, separated by ':'. * - * The only defined key (as of 2020-05-31) is ‘id’, which is used + * The only defined key (as of 2020-05-31) is 'id', which is used * to group split-up URIs: * * ╔═ file1 ════╗ @@ -483,7 +483,7 @@ const char *value = operator + 1; - if (strcmp(key, "id") == 0) + if (streq(key, "id")) id = sdbm_hash(value); } @@ -524,6 +524,19 @@ 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) == (char32_t)-1) { + LOG_WARN("%s: notification title is not valid UTF-8, ignoring", title); + return; + } + + if (msg != NULL && mbsntoc32(NULL, msg, strlen(msg), 0) == (char32_t)-1) { + LOG_WARN("%s: notification message is not valid UTF-8, ignoring", msg); + return; + } + notify_notify(term, title, msg != NULL ? msg : ""); } @@ -893,7 +906,7 @@ term->grid->cursor.point.row, term->grid->cursor.point.col); - term->grid->cur_row->prompt_marker = true; + term->grid->cur_row->shell_integration.prompt_marker = true; break; case 'B': @@ -901,15 +914,37 @@ break; case 'C': - LOG_DBG("FTCS_COMMAND_EXECUTED"); + LOG_DBG("FTCS_COMMAND_EXECUTED: %dx%d", + term->grid->cursor.point.row, + term->grid->cursor.point.col); + term->grid->cur_row->shell_integration.cmd_start = term->grid->cursor.point.col; break; case 'D': - LOG_DBG("FTCS_COMMAND_FINISHED"); + LOG_DBG("FTCS_COMMAND_FINISHED: %dx%d", + term->grid->cursor.point.row, + term->grid->cursor.point.col); + term->grid->cur_row->shell_integration.cmd_end = term->grid->cursor.point.col; break; } break; + case 176: + if (string[0] == '?' && string[1] == '\0') { + 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); + break; + } + + term_set_app_id(term, string); + break; + case 555: osc_flash(term); break; diff -Nru foot-1.16.2/pgo/full-headless-cage.sh foot-1.17.2/pgo/full-headless-cage.sh --- foot-1.16.2/pgo/full-headless-cage.sh 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/pgo/full-headless-cage.sh 2024-04-17 09:26:45.000000000 +0000 @@ -10,5 +10,5 @@ XDG_RUNTIME_DIR="${runtime_dir}" WLR_RENDERER=pixman WLR_BACKENDS=headless cage "${srcdir}"/pgo/full-inner.sh "${srcdir}" "${blddir}" -# Cage’s exit code doesn’t reflect our script’s exit code +# Cage's exit code doesn't reflect our script's exit code [ -f "${blddir}"/pgo-ok ] || exit 1 diff -Nru foot-1.16.2/pgo/full-headless-sway.sh foot-1.17.2/pgo/full-headless-sway.sh --- foot-1.16.2/pgo/full-headless-sway.sh 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/pgo/full-headless-sway.sh 2024-04-17 09:26:45.000000000 +0000 @@ -17,8 +17,8 @@ # 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’ +# 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}" -# Sway’s exit code doesn’t reflect our script’s exit code +# Sway's exit code doesn't reflect our script's exit code [ -f "${blddir}"/pgo-ok ] || exit 1 diff -Nru foot-1.16.2/pgo/pgo.c foot-1.17.2/pgo/pgo.c --- foot-1.16.2/pgo/pgo.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/pgo/pgo.c 2024-04-17 09:26:45.000000000 +0000 @@ -60,7 +60,8 @@ } bool -render_resize_force(struct terminal *term, int width, int height) +render_resize( + struct terminal *term, int width, int height, uint8_t resize_options) { return true; } @@ -68,6 +69,7 @@ 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) {} bool render_xcursor_is_valid(const struct seat *seat, const char *cursor) @@ -173,7 +175,8 @@ void get_current_modifiers(const struct seat *seat, xkb_mod_mask_t *effective, - xkb_mod_mask_t *consumed, uint32_t key) {} + xkb_mod_mask_t *consumed, uint32_t key, + bool filter_locked) {} static struct key_binding_set kbd; static bool kbd_initialized = false; diff -Nru foot-1.16.2/pgo/pgo.sh foot-1.17.2/pgo/pgo.sh --- foot-1.16.2/pgo/pgo.sh 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/pgo/pgo.sh 2024-04-17 09:26:45.000000000 +0000 @@ -82,6 +82,7 @@ # echo "CFLAGS: ${CFLAGS}" export CFLAGS +export CCACHE_DISABLE=1 meson setup --buildtype=release -Db_lto=true "${@}" "${blddir}" "${srcdir}" if [ ${do_pgo} = yes ]; then @@ -97,8 +98,8 @@ ninja -C "${blddir}" # If fcft/tllist are subprojects, we need to ensure their tests - # have been executed, or we’ll get “profile count data file not - # found” errors. + # have been executed, or we'll get "profile count data file not + # found" errors. ninja -C "${blddir}" test # Run mode-dependent script to generate profiling data diff -Nru foot-1.16.2/render.c foot-1.17.2/render.c --- foot-1.16.2/render.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/render.c 2024-04-17 09:26:45.000000000 +0000 @@ -269,12 +269,12 @@ continue; if (term->colors.table[0 + i] == color) { - /* “Regular” color, return the corresponding “dim” */ + /* "Regular" color, return the corresponding "dim" */ return conf->colors.dim[i]; } else if (term->colors.table[8 + i] == color) { - /* “Bright” color, return the corresponding “regular” */ + /* "Bright" color, return the corresponding "regular" */ return term->colors.table[i]; } } @@ -305,8 +305,8 @@ } static void -draw_unfocused_block(const struct terminal *term, pixman_image_t *pix, - const pixman_color_t *color, int x, int y, int cell_cols) +draw_hollow_block(const struct terminal *term, pixman_image_t *pix, + const pixman_color_t *color, int x, int y, int cell_cols) { const int scale = (int)roundf(term->scale); const int width = min(min(scale, term->cell_width), term->cell_height); @@ -429,10 +429,23 @@ switch (term->cursor_style) { case CURSOR_BLOCK: - if (unlikely(!term->kbd_focus)) - draw_unfocused_block(term, pix, &cursor_color, x, y, cols); + 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, fg, x, y, cols); + return; + + case CURSOR_UNFOCUSED_NONE: + return; + } + } - else if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) { + if (likely(term->cursor_blink.state == CURSOR_BLINK_ON) || + !term->kbd_focus) + { *fg = text_color; pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, &cursor_color, 1, @@ -1073,7 +1086,7 @@ /* * TODO: remove this if re-enabling scroll damage when re-applying - * last frame’s damage (see reapply_old_damage() + * last frame's damage (see reapply_old_damage() */ pixman_region32_union_rect( &buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); @@ -1150,7 +1163,7 @@ /* * TODO: remove this if re-enabling scroll damage when re-applying - * last frame’s damage (see reapply_old_damage() + * last frame's damage (see reapply_old_damage() */ pixman_region32_union_rect( &buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); @@ -1288,7 +1301,7 @@ * If the last sixel row only partially covers the cell row, * 'erase' the cell by rendering them. * - * In all cases, do *not* clear the ‘dirty’ bit on the row, to + * In all cases, do *not* clear the 'dirty' bit on the row, to * ensure the regular renderer includes them in the damage * rect. */ @@ -1403,8 +1416,8 @@ { /* Cursor will be drawn *after* the pre-edit string, i.e. in * the cell *after*. This means we need to copy, and dirty, - * one extra cell from the original grid, or we’ll leave - * trailing “cursors” after us if the user deletes text while + * one extra cell from the original grid, or we'll leave + * trailing "cursors" after us if the user deletes text while * pre-editing */ cells_needed++; } @@ -1460,7 +1473,7 @@ * from grid), and mark all cells as dirty. This ensures they are * re-rendered when the pre-edit text is modified or removed. */ - struct cell *real_cells = malloc(cells_used * sizeof(real_cells[0])); + struct cell *real_cells = xmalloc(cells_used * sizeof(real_cells[0])); for (int i = 0; i < cells_used; i++) { xassert(col_idx + i < term->cols); real_cells[i] = row->cells[col_idx + i]; @@ -1513,7 +1526,7 @@ /* Hollow cursor */ if (start >= 0 && end <= term->cols) { int cols = end - start; - draw_unfocused_block(term, buf->pix[0], &cursor_color, x, y, cols); + draw_hollow_block(term, buf->pix[0], &cursor_color, x, y, cols); } term_ime_set_cursor_rect( @@ -1583,7 +1596,7 @@ } struct buffer *buf = shm_get_buffer( - term->render.chains.overlay, term->width, term->height); + term->render.chains.overlay, term->width, term->height, true); pixman_image_set_clip_region32(buf->pix[0], NULL); @@ -1613,23 +1626,23 @@ * When possible, we only update the areas that have *changed* * since the last frame. That means: * - * - clearing/erasing cells that are now selected, but weren’t + * - clearing/erasing cells that are now selected, but weren't * in the last frame - * - dimming cells that were selected, but aren’t anymore + * - dimming cells that were selected, but aren't anymore * - * To do this, we save the last frame’s selected cells as a + * To do this, we save the last frame's selected cells as a * pixman region. * * Then, we calculate the corresponding region for this - * frame’s selected cells. + * frame's selected cells. * - * Last frame’s region minus this frame’s region gives us the + * Last frame's region minus this frame's region gives us the * region that needs to be *dimmed* in this frame * - * This frame’s region minus last frame’s region gives us the + * This frame's region minus last frame's region gives us the * region that needs to be *cleared* in this frame. * - * Finally, the union of the two “diff” regions above, gives + * Finally, the union of the two "diff" regions above, gives * us the total region affected by a change, in either way. We * use this as the bounding box for the * wl_surface_damage_buffer() call. @@ -1642,12 +1655,12 @@ buf->age == 0; if (!buffer_reuse) { - /* Can’t reuse last frame’s damage - set to full window, + /* Can't reuse last frame's damage - set to full window, * to ensure *everything* is updated */ pixman_region32_init_rect( &old_see_through, 0, 0, buf->width, buf->height); } else { - /* Use last frame’s saved region */ + /* Use last frame's saved region */ pixman_region32_init(&old_see_through); pixman_region32_copy(&old_see_through, see_through); } @@ -2072,7 +2085,7 @@ } /* - * The “visible” border. + * The "visible" border. */ float scale = term->scale; @@ -2454,7 +2467,7 @@ } struct buffer *bufs[CSD_SURF_COUNT]; - shm_get_many(term->render.chains.csd, CSD_SURF_COUNT, widths, heights, bufs); + shm_get_many(term->render.chains.csd, CSD_SURF_COUNT, widths, heights, bufs, true); for (size_t i = CSD_SURF_LEFT; i <= CSD_SURF_BOTTOM; i++) render_csd_border(term, i, &infos[i], bufs[i]); @@ -2599,7 +2612,7 @@ } struct buffer_chain *chain = term->render.chains.scrollback_indicator; - struct buffer *buf = shm_get_buffer(chain, width, height); + struct buffer *buf = shm_get_buffer(chain, width, height, false); wl_subsurface_set_position( win->scrollback_indicator.sub, roundf(x / scale), roundf(y / scale)); @@ -2642,7 +2655,7 @@ height = roundf(scale * ceilf(height / scale)); struct buffer_chain *chain = term->render.chains.render_timer; - struct buffer *buf = shm_get_buffer(chain, width, height); + struct buffer *buf = shm_get_buffer(chain, width, height, false); wl_subsurface_set_position( win->render_timer.sub, @@ -2692,21 +2705,21 @@ pixman_region32_init(&dirty); /* - * Figure out current frame’s damage region + * Figure out current frame's damage region * - * If current frame doesn’t have any scroll damage, we can simply - * subtract this frame’s damage from the last frame’s damage. That - * way, we don’t have to copy areas from the old frame that’ll + * If current frame doesn't have any scroll damage, we can simply + * subtract this frame's damage from the last frame's damage. That + * way, we don't have to copy areas from the old frame that'll * just get overwritten by current frame. * - * Note that this is row based. A “half damaged” row is not + * Note that this is row based. A "half damaged" row is not * excluded. I.e. the entire row will be copied from the old frame * to the new, and then when actually rendering the new frame, the * updated cells will overwrite parts of the copied row. * - * Since we’re scanning the entire viewport anyway, we also track + * Since we're scanning the entire viewport anyway, we also track * whether *all* cells are to be updated. In this case, just force - * a full re-rendering, and don’t copy anything from the old + * a full re-rendering, and don't copy anything from the old * frame. */ bool full_repaint_needed = true; @@ -2739,28 +2752,28 @@ } /* - * TODO: re-apply last frame’s scroll damage + * TODO: re-apply last frame's scroll damage * * We used to do this, but it turned out to be buggy. If we decide - * to re-add it, this is where to do it. Note that we’d also have + * to re-add it, this is where to do it. Note that we'd also have * to remove the updates to buf->dirty from grid_render_scroll() * and grid_render_scroll_reverse(). */ if (tll_length(term->grid->scroll_damage) == 0) { /* - * We can only subtract current frame’s damage from the old - * frame’s if we don’t have any scroll damage. + * We can only subtract current frame's damage from the old + * frame's if we don't have any scroll damage. * * If we do have scroll damage, the damage region we * calculated above is not (yet) valid - we need to apply the - * current frame’s scroll damage *first*. This is done later, + * current frame's scroll damage *first*. This is done later, * when rendering the frame. */ pixman_region32_subtract(&dirty, &old->dirty[0], &dirty); pixman_image_set_clip_region32(new->pix[0], &dirty); } else { - /* Copy *all* of last frame’s damaged areas */ + /* Copy *all* of last frame's damaged areas */ pixman_image_set_clip_region32(new->pix[0], &old->dirty[0]); } @@ -2817,9 +2830,12 @@ xassert(term->height > 0); struct buffer_chain *chain = term->render.chains.grid; - struct buffer *buf = shm_get_buffer(chain, term->width, term->height); + bool use_alpha = !term->window->is_fullscreen && + term->colors.alpha != 0xffff; + struct buffer *buf = shm_get_buffer( + chain, term->width, term->height, use_alpha); - /* Dirty old and current cursor cell, to ensure they’re repainted */ + /* Dirty old and current cursor cell, to ensure they're repainted */ dirty_old_cursor(term); dirty_cursor(term); @@ -2938,11 +2954,11 @@ * they are overflowing. * * As soon as we see a non-overflowing cell we can - * stop, since it isn’t affecting the string of + * stop, since it isn't affecting the string of * overflowing glyphs that follows it. * * As soon as we see a dirty cell, we can stop, since - * that means we’ve already handled it (remember the + * that means we've already handled it (remember the * outer loop goes from left to right). */ for (struct cell *c = cell - 1; c >= &row->cells[0]; c--) { @@ -2960,9 +2976,9 @@ * Note that the first non-overflowing cell must be * re-rendered as well, but any cell *after* that is * unaffected by the string of overflowing glyphs - * we’re dealing with right now. + * we're dealing with right now. * - * For performance, this iterates the *outer* loop’s + * For performance, this iterates the *outer* loop's * cell pointer - no point in re-checking all these * glyphs again, in the outer loop. */ @@ -3137,9 +3153,9 @@ /* * We treat the search box pretty much like a row of cells. That - * is, a glyph is either 1 or 2 (or more) “cells” wide. + * is, a glyph is either 1 or 2 (or more) "cells" wide. * - * The search ‘length’, and ‘cursor’ (position) is in + * The search 'length', and 'cursor' (position) is in * *characters*, not cells. This means we need to translate from * character count to cell count when calculating the length of * the search box, where in the search string we should start @@ -3208,7 +3224,7 @@ 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); + struct buffer *buf = shm_get_buffer(chain, width, height, true); pixman_region32_t clip; pixman_region32_init_rect(&clip, 0, 0, width, height); @@ -3307,7 +3323,7 @@ } /* - * Render the search string, starting at ‘glyph_offset’. Note that + * Render the search string, starting at 'glyph_offset'. Note that * glyph_offset is in cells, not characters */ for (size_t i = 0, @@ -3370,7 +3386,7 @@ /* TODO: how do we handle a partially hidden rectangle? */ if (start >= 0 && end <= visible_cells) { - draw_unfocused_block( + draw_hollow_block( term, buf->pix[0], &fg, x + start * term->cell_width, y, end - start); } term_ime_set_cursor_rect(term, @@ -3580,7 +3596,7 @@ int x = col * term->cell_width - 15 * term->cell_width / 10; int y = row * term->cell_height - 5 * term->cell_height / 10; - /* Don’t position it outside our window */ + /* Don't position it outside our window */ if (x < -term->margins.left) x = -term->margins.left; if (y < -term->margins.top) @@ -3621,12 +3637,12 @@ label[i] = U' '; /* - * Don’t extend outside our window + * Don't extend outside our window * - * Truncate label so that it doesn’t extend outside our + * Truncate label so that it doesn't extend outside our * window. * - * Do it in a way such that we don’t cut the label in the + * Do it in a way such that we don't cut the label in the * middle of a double-width character. */ @@ -3670,7 +3686,7 @@ struct buffer_chain *chain = term->render.chains.url; struct buffer *bufs[render_count]; - shm_get_many(chain, render_count, widths, heights, bufs); + shm_get_many(chain, render_count, widths, heights, bufs, false); uint32_t fg = term->conf->colors.use_custom.jump_label ? term->conf->colors.jump_label.fg @@ -3805,7 +3821,7 @@ term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, tracking_points); - /* Replace the current, truncated, “normal” grid with the + /* Replace the current, truncated, "normal" grid with the * correctly reflowed one */ grid_free(&term->normal); term->normal = *term->interactive_resizing.grid; @@ -3868,7 +3884,7 @@ win->resize_timeout_fd = -1; } } else { - /* Send new dimensions to client “in a while” */ + /* Send new dimensions to client "in a while" */ assert(win->is_resizing && term->conf->resize_delay_ms > 0); int fd = win->resize_timeout_fd; @@ -3912,9 +3928,25 @@ } } +static void +set_size_from_grid(struct terminal *term, int *width, int *height, int cols, int rows) +{ + /* Nominal grid dimensions */ + *width = cols * term->cell_width; + *height = rows * term->cell_height; + + /* Include any configured padding */ + *width += 2 * term->conf->pad_x * term->scale; + *height += 2 * term->conf->pad_y * term->scale; + + /* Round to multiples of scale */ + *width = round(term->scale * round(*width / term->scale)); + *height = round(term->scale * round(*height / term->scale)); +} + /* Move to terminal.c? */ -static bool -maybe_resize(struct terminal *term, int width, int height, bool force) +bool +render_resize(struct terminal *term, int width, int height, uint8_t opts) { if (term->shutdown.in_progress) return false; @@ -3925,21 +3957,29 @@ 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 && height == 0) { - /* - * The compositor is letting us choose the size - * - * If we have a "last" used size - use that. Otherwise, use - * the size from the user configuration. - */ + /* The compositor is letting us choose the size */ if (term->stashed_width != 0 && term->stashed_height != 0) { + /* If a default size is requested, prefer the "last used" size */ width = term->stashed_width; height = term->stashed_height; } else { + /* Otherwise, use a user-configured size */ switch (term->conf->size.type) { case CONF_SIZE_PX: width = term->conf->size.width; @@ -3959,15 +3999,8 @@ break; case CONF_SIZE_CELLS: - width = term->conf->size.width * term->cell_width; - height = term->conf->size.height * term->cell_height; - - width += 2 * term->conf->pad_x * scale; - height += 2 * term->conf->pad_y * scale; - - /* Ensure width/height is a valid multiple of scale */ - width = roundf(scale * roundf(width / scale)); - height = roundf(scale * roundf(height / scale)); + set_size_from_grid(term, &width, &height, + term->conf->size.width, term->conf->size.height); break; } } @@ -3990,8 +4023,25 @@ const int pad_x = min(max_pad_x, scale * term->conf->pad_x); const int pad_y = min(max_pad_y, scale * term->conf->pad_y); - if (!force && width == term->width && height == term->height && scale == term->scale) + if (is_floating && + (opts & RESIZE_BY_CELLS) && + term->conf->resize_by_cells) + { + /* If resizing in cell increments, restrict the width and height */ + width = ((width - 2 * pad_x) / term->cell_width) * term->cell_width + 2 * pad_x; + width = max(min_width, roundf(scale * roundf(width / scale))); + + height = ((height - 2 * pad_y) / term->cell_height) * term->cell_height + 2 * pad_y; + height = max(min_height, roundf(scale * roundf(height / scale))); + } + + if (!(opts & RESIZE_FORCE) && + width == term->width && + height == term->height && + scale == term->scale) + { return false; + } /* Cancel an application initiated "Synchronized Update" */ term_disable_app_sync_updates(term); @@ -4026,7 +4076,11 @@ const int total_x_pad = term->width - grid_width; const int total_y_pad = term->height - grid_height; - if (term->conf->center && !term->window->is_resizing) { + const bool centered_padding = term->conf->center + || term->window->is_fullscreen + || term->window->is_maximized; + + if (centered_padding && !term->window->is_resizing) { term->margins.left = total_x_pad / 2; term->margins.top = total_y_pad / 2; } else { @@ -4049,8 +4103,8 @@ /* - * Since text reflow is slow, don’t do it *while* resizing. Only - * do it when done, or after “pausing” the resize for sufficiently + * 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(). * @@ -4061,7 +4115,7 @@ if (term->interactive_resizing.grid == NULL) { term_ptmx_pause(term); - /* Stash the current ‘normal’ grid, as-is, to be used when + /* Stash the current 'normal' grid, as-is, to be used when * doing the final reflow */ term->interactive_resizing.old_screen_rows = term->rows; term->interactive_resizing.old_cols = term->cols; @@ -4072,7 +4126,7 @@ if (term->grid == &term->normal) term->interactive_resizing.selection_coords = term->selection.coords; } else { - /* We’ll replace the current temporary grid, with a new + /* We'll replace the current temporary grid, with a new * one (again based on the original grid) */ grid_free(&term->normal); } @@ -4082,12 +4136,12 @@ /* * Copy the current viewport (of the original grid) to a new * grid that will be used during the resize. For now, throw - * away sixels and OSC-8 URLs. They’ll be "restored" when we + * away sixels and OSC-8 URLs. They'll be "restored" when we * do the final reflow. * * Note that OSC-8 URLs are perfectly ok to throw away; they * cannot be interacted with during the resize. And, even if - * url.osc8-underline=always, the “underline” attribute is + * url.osc8-underline=always, the "underline" attribute is * part of the cell, not the URI struct (and thus our faked * grid will still render OSC-8 links underlined). * @@ -4129,7 +4183,7 @@ selection_cancel(term); else { /* - * Don’t cancel, but make sure there aren’t any ongoing + * Don't cancel, but make sure there aren't any ongoing * selections after the resize. */ tll_foreach(term->wl->seats, it) { @@ -4141,7 +4195,7 @@ /* * TODO: if we remove the selection_finalize() call above (i.e. if * we start allowing selections to be ongoing across resizes), the - * selection’s pivot point coordinates *must* be added to the + * selection's pivot point coordinates *must* be added to the * tracking points list. */ /* Resize grids */ @@ -4161,7 +4215,7 @@ int old_normal_rows = old_rows; if (term->interactive_resizing.grid != NULL) { - /* Throw away the current, truncated, “normal” grid, and + /* Throw away the current, truncated, "normal" grid, and * use the original grid instead (from before the resize * started) */ grid_free(&term->normal); @@ -4225,10 +4279,7 @@ /* Signal TIOCSWINSZ */ send_dimensions_to_client(term); - if (!term->window->is_maximized && - !term->window->is_fullscreen && - !term->window->is_tiled) - { + if (is_floating) { /* Stash current size, to enable us to restore it when we're * being un-maximized/fullscreened/tiled */ term->stashed_width = term->width; @@ -4291,18 +4342,6 @@ return true; } -bool -render_resize(struct terminal *term, int width, int height) -{ - return maybe_resize(term, width, height, false); -} - -bool -render_resize_force(struct terminal *term, int width, int height) -{ - return maybe_resize(term, width, height, true); -} - static void xcursor_callback( void *data, struct wl_callback *wl_callback, uint32_t callback_data); static const struct wl_callback_listener xcursor_listener = { @@ -4337,8 +4376,6 @@ return; } - xassert(seat->pointer.cursor != NULL); - const enum cursor_shape shape = seat->pointer.shape; const char *const xcursor = seat->pointer.last_custom_xcursor; @@ -4372,6 +4409,23 @@ 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); @@ -4489,9 +4543,6 @@ void render_refresh_title(struct terminal *term) { - if (term->render.title.is_armed) - return; - struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) return; @@ -4514,6 +4565,28 @@ } 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); + } else { + term->render.app_id.last_update = now; + xdg_toplevel_set_app_id(term->window->xdg_toplevel, term->app_id ? term->app_id : term->conf->app_id); + } +} + +void render_refresh(struct terminal *term) { term->render.refresh.grid = true; @@ -4544,7 +4617,7 @@ render_xcursor_set(struct seat *seat, struct terminal *term, enum cursor_shape shape) { - if (seat->pointer.theme == NULL) + if (seat->pointer.theme == NULL && seat->pointer.shape_device == NULL) return false; if (seat->mouse_focus == NULL) { @@ -4559,42 +4632,52 @@ if (seat->pointer.shape == shape && !(shape == CURSOR_SHAPE_CUSTOM && - strcmp(seat->pointer.last_custom_xcursor, - term->mouse_user_cursor) != 0)) + !streq(seat->pointer.last_custom_xcursor, + term->mouse_user_cursor))) { return true; } - /* TODO: skip this when using server-side cursors */ - if (shape != CURSOR_SHAPE_HIDDEN) { - const char *const xcursor = shape == CURSOR_SHAPE_CUSTOM - ? term->mouse_user_cursor + 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); - const char *const fallback = - cursor_shape_to_string(CURSOR_SHAPE_TEXT_FALLBACK); - seat->pointer.cursor = wl_cursor_theme_get_cursor( - seat->pointer.theme, xcursor); + xassert(xcursors[0] != NULL); - if (seat->pointer.cursor == NULL) { - seat->pointer.cursor = wl_cursor_theme_get_cursor( - seat->pointer.theme, fallback); + 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_ERR("failed to load xcursor pointer " - "'%s', and fallback '%s'", xcursor, fallback); - return false; + if (seat->pointer.cursor != NULL) { + LOG_DBG("loaded xcursor %s", xcursors[i]); + break; } } - if (shape == CURSOR_SHAPE_CUSTOM) { - free(seat->pointer.last_custom_xcursor); - seat->pointer.last_custom_xcursor = xstrdup(term->mouse_user_cursor); + if (seat->pointer.cursor == NULL) { + LOG_ERR( + "failed to load xcursor pointer '%s', and all of its fallbacks", + xcursors[0]); + return false; } } else { - seat->pointer.cursor = NULL; + /* Server-side cursors - no need to load anything */ + } + + if (shape == CURSOR_SHAPE_CUSTOM) { free(seat->pointer.last_custom_xcursor); - seat->pointer.last_custom_xcursor = NULL; + seat->pointer.last_custom_xcursor = + xstrdup(term->mouse_user_cursor); } /* FDM hook takes care of actual rendering */ diff -Nru foot-1.16.2/render.h foot-1.17.2/render.h --- foot-1.16.2/render.h 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/render.h 2024-04-17 09:26:45.000000000 +0000 @@ -10,10 +10,18 @@ struct renderer *render_init(struct fdm *fdm, struct wayland *wayl); void render_destroy(struct renderer *renderer); -bool render_resize(struct terminal *term, int width, int height); -bool render_resize_force(struct terminal *term, int width, int height); +enum resize_options { + RESIZE_NORMAL = 0, + RESIZE_FORCE = 1 << 0, + RESIZE_BY_CELLS = 1 << 1, + RESIZE_KEEP_GRID = 1 << 2, +}; + +bool render_resize( + struct terminal *term, int width, int height, uint8_t resize_options); void render_refresh(struct terminal *term); +void render_refresh_app_id(struct terminal *term); void render_refresh_csd(struct terminal *term); void render_refresh_search(struct terminal *term); void render_refresh_title(struct terminal *term); diff -Nru foot-1.16.2/scripts/generate-alt-random-writes.py foot-1.17.2/scripts/generate-alt-random-writes.py --- foot-1.16.2/scripts/generate-alt-random-writes.py 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/scripts/generate-alt-random-writes.py 2024-04-17 09:26:45.000000000 +0000 @@ -207,8 +207,9 @@ six_height, six_width = last_size six_rows = (six_height + 5) // 6 # Round up; each sixel is 6 pixels - # Begin sixel (with P2=1 - empty sixels are transparent) - out.write('\033P;1q') + # 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. diff -Nru foot-1.16.2/search.c foot-1.17.2/search.c --- foot-1.16.2/search.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/search.c 2024-04-17 09:26:45.000000000 +0000 @@ -1374,17 +1374,12 @@ search_input(struct seat *seat, struct terminal *term, const struct key_binding_set *bindings, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, - xkb_mod_mask_t locked, const xkb_keysym_t *raw_syms, size_t raw_count, uint32_t serial) { LOG_DBG("search: input: sym=%d/0x%x, mods=0x%08x, consumed=0x%08x", sym, sym, mods, consumed); - const xkb_mod_mask_t bind_mods = - mods & seat->kbd.bind_significant & ~locked; - const xkb_mod_mask_t bind_consumed = - consumed & seat->kbd.bind_significant & ~locked; enum xkb_compose_status compose_status = seat->kbd.xkb_compose_state != NULL ? xkb_compose_state_get_status(seat->kbd.xkb_compose_state) : XKB_COMPOSE_NOTHING; @@ -1399,7 +1394,7 @@ /* Match translated symbol */ if (bind->k.sym == sym && - bind->mods == (bind_mods & ~bind_consumed)) { + bind->mods == (mods & ~consumed)) { if (execute_binding(seat, term, bind, serial, &update_search_result, &search_direction, @@ -1410,7 +1405,7 @@ return; } - if (bind->mods != bind_mods || bind_mods != (mods & ~locked)) + if (bind->mods != mods) continue; /* Match untranslated symbols */ diff -Nru foot-1.16.2/search.h foot-1.17.2/search.h --- foot-1.16.2/search.h 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/search.h 2024-04-17 09:26:45.000000000 +0000 @@ -11,7 +11,6 @@ struct seat *seat, struct terminal *term, const struct key_binding_set *bindings, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, - xkb_mod_mask_t locked, const xkb_keysym_t *raw_syms, size_t raw_count, uint32_t serial); void search_add_chars(struct terminal *term, const char *text, size_t len); diff -Nru foot-1.16.2/selection.c foot-1.17.2/selection.c --- foot-1.16.2/selection.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/selection.c 2024-04-17 09:26:45.000000000 +0000 @@ -858,8 +858,8 @@ return region; } -/* Returns a pixman region representing the selection between ‘start’ - * and ‘end’ (given the current selection kind), in *scrollback +/* Returns a pixman region representing the selection between 'start' + * and 'end' (given the current selection kind), in *scrollback * relative coordinates* */ static pixman_region32_t pixman_region_for_coords(const struct terminal *term, @@ -921,17 +921,17 @@ * followed by non-empty cell(s), since this * corresponds to what gets extracted when the * selection is copied (that is, empty cells - * “between” non-empty cells are converted to + * "between" non-empty cells are converted to * spaces). * * However, they way we handle selection updates - * (diffing the “old” selection area against the - * “new” one, using pixman regions), means we - * can’t correctly update the state of empty - * cells. The result is “random” empty cells being - * rendered as selected when they shouldn’t. + * (diffing the "old" selection area against the + * "new" one, using pixman regions), means we + * can't correctly update the state of empty + * cells. The result is "random" empty cells being + * rendered as selected when they shouldn't. * - * “Fix” by *never* highlighting selected empty + * "Fix" by *never* highlighting selected empty * cells (they still get converted to spaces when * copied, if followed by non-empty cells). */ @@ -944,8 +944,8 @@ * * This is due to how the algorithm for updating * the selection works; it uses regions to - * calculate the difference between the “old” and - * the “new” selection. This makes it impossible + * calculate the difference between the "old" and + * the "new" selection. This makes it impossible * to tell if an empty cell is a *trailing* empty * cell (that should not be highlighted), or an * empty cells between non-empty cells (that @@ -957,7 +957,7 @@ * empty cell is trailing or not. * * So, what we need to do is check if a - * ‘selected’, and empty cell has been marked as + * 'selected', and empty cell has been marked as * selected, temporarily unmark (forcing it dirty, * to ensure it gets re-rendered). If it is *not* * a trailing empty cell, it will get re-tagged as @@ -1042,7 +1042,7 @@ *pivot_start = start; - /* First, make sure ‘start’ isn’t in the middle of a + /* First, make sure 'start' isn't in the middle of a * multi-column character */ while (true) { const struct row *row = term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)]; @@ -1051,7 +1051,7 @@ if (cell->wc < CELL_SPACER) break; - /* Multi-column chars don’t cross rows */ + /* Multi-column chars don't cross rows */ xassert(pivot_start->col > 0); if (pivot_start->col == 0) break; @@ -1876,7 +1876,7 @@ clipboard->text = NULL; } -/* We don’t support dragging *from* */ +/* We don't support dragging *from* */ static void dnd_drop_performed(void *data, struct wl_data_source *wl_data_source) { @@ -2080,7 +2080,7 @@ ctx->cb(" ", 1, ctx->user); ctx->add_space = true; - if (strcmp(scheme, "file") == 0 && hostname_is_localhost(host)) { + if (streq(scheme, "file") && hostname_is_localhost(host)) { if (ctx->quote_paths) ctx->cb("'", 1, ctx->user); @@ -2218,11 +2218,11 @@ /* * In addition to stripping non-formatting C0 controls, - * XTerm has an option, “disallowedPasteControls”, that + * XTerm has an option, "disallowedPasteControls", that * defines C0 controls that will be replaced with spaces * when pasted. * - * It’s default value is BS,DEL,ENQ,EOT,NUL + * It's default value is BS,DEL,ENQ,EOT,NUL * * Instead of replacing them with spaces, we allow them in * bracketed paste mode, and strip them completely in @@ -2534,7 +2534,7 @@ if (mime_type_map[i] == NULL) continue; - if (strcmp(_mime_type, mime_type_map[i]) == 0) { + if (streq(_mime_type, mime_type_map[i])) { mime_type = i; break; } @@ -2716,7 +2716,7 @@ reject_offer: /* Either terminal is already busy sending paste data, or mouse - * pointer isn’t over the grid */ + * pointer isn't over the grid */ seat->clipboard.window = NULL; wl_data_offer_accept(offer, serial, NULL); wl_data_offer_set_actions( @@ -2812,7 +2812,7 @@ term, read_fd, clipboard->mime_type, &receive_dnd, &receive_dnd_done, ctx); - /* data offer is now “owned” by the receive context */ + /* data offer is now "owned" by the receive context */ clipboard->data_offer = NULL; clipboard->mime_type = DATA_OFFER_MIME_UNSET; } diff -Nru foot-1.16.2/server.c foot-1.17.2/server.c --- foot-1.16.2/server.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/server.c 2024-04-17 09:26:45.000000000 +0000 @@ -301,7 +301,7 @@ #undef CHECK_BUF_AND_NULL #undef CHECK_BUF - struct terminal_instance *instance = malloc(sizeof(struct terminal_instance)); + struct terminal_instance *instance = xmalloc(sizeof(struct terminal_instance)); const bool need_to_clone_conf = tll_length(overrides)> 0 || @@ -332,7 +332,8 @@ instance->terminal = term_init( conf != NULL ? conf : server->conf, server->fdm, server->reaper, server->wayl, "footclient", cwd, token, - cdata.argc, argv, envp, &term_shutdown_handler, instance); + NULL, cdata.argc, argv, (const char *const *)envp, + &term_shutdown_handler, instance); if (instance->terminal == NULL) { LOG_ERR("failed to instantiate new terminal"); diff -Nru foot-1.16.2/shm.c foot-1.17.2/shm.c --- foot-1.16.2/shm.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/shm.c 2024-04-17 09:26:45.000000000 +0000 @@ -82,6 +82,7 @@ struct buffer_pool *pool; off_t offset; /* Offset into memfd where data begins */ size_t size; + bool with_alpha; bool scrollable; }; @@ -261,7 +262,7 @@ wl_buf = wl_shm_pool_create_buffer( pool->wl_pool, new_offset, buf->public.width, buf->public.height, buf->public.stride, - WL_SHM_FORMAT_ARGB8888); + buf->with_alpha ? WL_SHM_FORMAT_ARGB8888 : WL_SHM_FORMAT_XRGB8888); if (wl_buf == NULL) { LOG_ERR("failed to create SHM buffer"); @@ -271,7 +272,8 @@ /* One pixman image for each worker thread (do we really need multiple?) */ for (size_t i = 0; i < buf->public.pix_instances; i++) { pix[i] = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, buf->public.width, buf->public.height, + buf->with_alpha ? PIXMAN_a8r8g8b8 : PIXMAN_x8r8g8b8, + buf->public.width, buf->public.height, (uint32_t *)mmapped, buf->public.stride); if (pix[i] == NULL) { LOG_ERR("failed to create pixman image"); @@ -304,7 +306,8 @@ 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) + struct buffer *bufs[static count], bool with_alpha, + bool immediate_purge) { xassert(count == 1 || !chain->scrollable); /* @@ -322,7 +325,8 @@ size_t total_size = 0; for (size_t i = 0; i < count; i++) { - stride[i] = stride_for_format_and_width(PIXMAN_a8r8g8b8, widths[i]); + stride[i] = stride_for_format_and_width( + with_alpha ? PIXMAN_a8r8g8b8 : PIXMAN_x8r8g8b8, widths[i]); sizes[i] = stride[i] * heights[i]; total_size += sizes[i]; } @@ -473,6 +477,7 @@ .chain = chain, .ref_count = immediate_purge ? 0 : 1, .busy = true, + .with_alpha = with_alpha, .pool = pool, .offset = 0, .size = sizes[i], @@ -489,7 +494,7 @@ else tll_push_front(chain->bufs, buf); - buf->public.dirty = malloc( + buf->public.dirty = xmalloc( chain->pix_instances * sizeof(buf->public.dirty[0])); for (size_t j = 0; j < chain->pix_instances; j++) @@ -511,7 +516,7 @@ #endif if (!(bufs[0] && shm_can_scroll(bufs[0]))) { - /* We only need to keep the pool FD open if we’re going to SHM + /* We only need to keep the pool FD open if we're going to SHM * scroll it */ close(pool_fd); pool->fd = -1; @@ -542,13 +547,13 @@ void shm_get_many(struct buffer_chain *chain, size_t count, int widths[static count], int heights[static count], - struct buffer *bufs[static count]) + struct buffer *bufs[static count], bool with_alpha) { - get_new_buffers(chain, count, widths, heights, bufs, true); + get_new_buffers(chain, count, widths, heights, bufs, with_alpha, true); } struct buffer * -shm_get_buffer(struct buffer_chain *chain, int width, int height) +shm_get_buffer(struct buffer_chain *chain, int width, int height, bool with_alpha) { LOG_DBG( "chain=%p: looking for a reusable %dx%d buffer " @@ -579,7 +584,7 @@ cached = buf; else { /* We have multiple buffers eligible for - * reuse. Pick the “youngest” one, and mark the + * reuse. Pick the "youngest" one, and mark the * other one for purging */ if (buf->public.age < cached->public.age) { shm_unref(&cached->public); @@ -589,8 +594,8 @@ * TODO: I think we _can_ use shm_unref() * here... * - * shm_unref() may remove ‘it’, but that - * should be safe; “our” tll_foreach() already + * shm_unref() may remove 'it', but that + * should be safe; "our" tll_foreach() already * holds the next pointer. */ if (buffer_unref_no_remove_from_chain(buf)) @@ -610,7 +615,7 @@ } struct buffer *ret; - get_new_buffers(chain, 1, &width, &height, &ret, false); + get_new_buffers(chain, 1, &width, &height, &ret, with_alpha, false); return ret; } diff -Nru foot-1.16.2/shm.h foot-1.17.2/shm.h --- foot-1.16.2/shm.h 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/shm.h 2024-04-17 09:26:45.000000000 +0000 @@ -49,15 +49,16 @@ /* * Returns a single buffer. * - * May returned a cached buffer. If so, the buffer’s age indicates how + * May returned a cached buffer. If so, the buffer's age indicates how * many shm_get_buffer() calls have been made for the same * width/height while the buffer was still busy. * * A newly allocated buffer has an age of 1234. */ -struct buffer *shm_get_buffer(struct buffer_chain *chain, int width, int height); +struct buffer *shm_get_buffer( + struct buffer_chain *chain, int width, int height, bool with_alpha); /* - * Returns many buffers, described by ‘info’, all sharing the same SHM + * Returns many buffers, described by 'info', all sharing the same SHM * buffer pool. * * Never returns cached buffers. However, the newly created buffers @@ -73,7 +74,7 @@ void shm_get_many( struct buffer_chain *chain, size_t count, int widths[static count], int heights[static count], - struct buffer *bufs[static count]); + struct buffer *bufs[static count], bool with_alpha); void shm_did_not_use_buf(struct buffer *buf); diff -Nru foot-1.16.2/sixel.c foot-1.17.2/sixel.c --- foot-1.16.2/sixel.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/sixel.c 2024-04-17 09:26:45.000000000 +0000 @@ -19,6 +19,29 @@ static void sixel_put_generic(struct terminal *term, uint8_t c); static void sixel_put_ar_11(struct terminal *term, uint8_t c); +/* VT330/VT340 Programmer Reference Manual - Table 2-3 VT340 Default Color Map */ +static const uint32_t vt340_default_colors[16] = { + 0xff000000, + 0xff3333cc, + 0xffcc2121, + 0xff33cc33, + 0xffcc33cc, + 0xff33cccc, + 0xffcccc33, + 0xff878787, + 0xff424242, + 0xff545499, + 0xff994242, + 0xff549954, + 0xff995499, + 0xff549999, + 0xff999954, + 0xffcccccc, +}; + +_Static_assert(sizeof(vt340_default_colors) / sizeof(vt340_default_colors[0]) == 16, + "wrong number of elements"); + void sixel_fini(struct terminal *term) { @@ -56,7 +79,6 @@ term->sixel.state = SIXEL_DECSIXEL; term->sixel.pos = (struct coord){0, 0}; - term->sixel.row_byte_ofs = 0; term->sixel.color_idx = 0; term->sixel.pan = pan; term->sixel.pad = pad; @@ -65,20 +87,33 @@ 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; - - /* TODO: default palette */ + term->sixel.image.alloc_height = 0; + term->sixel.image.bottom_pixel = 0; 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, vt340_default_colors, + min(sizeof(vt340_default_colors), + term->sixel.palette_size * sizeof(term->sixel.private_palette[0]))); + 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, vt340_default_colors, + min(sizeof(vt340_default_colors), + term->sixel.palette_size * sizeof(term->sixel.shared_palette[0]))); } else { /* Shared palette - do *not* reset palette for new sixels */ } @@ -613,7 +648,8 @@ pixman_region32_t cell_intersection; pixman_region32_init(&cell_intersection); pixman_region32_intersect(&cell_intersection, &six_rect, &overwrite_rect); - xassert(pixman_region32_not_empty(&cell_intersection)); + xassert(!pixman_region32_not_empty(&six_rect) || + pixman_region32_not_empty(&cell_intersection)); pixman_region32_fini(&cell_intersection); #endif @@ -979,7 +1015,7 @@ struct grid *active_grid = term->grid; term->grid = grid; - /* Need the “real” list to be empty from the beginning */ + /* Need the "real" list to be empty from the beginning */ tll(struct sixel) copy = tll_init(); tll_foreach(grid->sixel_images, it) tll_push_back(copy, it->item); @@ -1028,7 +1064,7 @@ continue; } - /* Sixels that didn’t overlap may now do so, which isn’t + /* Sixels that didn't overlap may now do so, which isn't * allowed of course */ _sixel_overwrite_by_rectangle( term, six->pos.row, six->pos.col, six->rows, six->cols, @@ -1062,6 +1098,81 @@ 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); @@ -1096,10 +1207,9 @@ int start_row = do_scroll ? term->grid->cursor.point.row : 0; const int start_col = do_scroll ? term->grid->cursor.point.col : 0; - /* Total number of rows needed by image (+ optional newline at the end) */ + /* Total number of rows needed by image */ const int rows_needed = - (term->sixel.image.height + term->cell_height - 1) / term->cell_height + - (term->sixel.cursor_right_of_graphics ? 0 : 1); + (term->sixel.image.height + term->cell_height - 1) / term->cell_height; bool free_image_data = true; @@ -1162,7 +1272,7 @@ LOG_DBG("generating %s %dx%d pixman image at %d-%d", image.opaque ? "opaque" : "transparent", - image.width, image.height, + image.original.width, image.original.height, image.pos.row, image.pos.row + image.rows); image.original.pix = pixman_image_create_bits_no_clear( @@ -1197,19 +1307,14 @@ int row = term->grid->cursor.point.row; /* - * Position the text cursor based on the **upper** - * pixel, of the last sixel. - * - * In most cases, that’ll end up being the very last - * row of the sixel (which we’re already at, thanks to - * the linefeeds). But for some combinations of font - * and image sizes, the final cursor position is - * higher up. + * Position the text cursor based on the text row + * touched by the last sixel */ - const int sixel_row_height = 6 * term->sixel.pan; - const int sixel_rows = (image.original.height + sixel_row_height - 1) / sixel_row_height; - const int upper_pixel_last_sixel = (sixel_rows - 1) * sixel_row_height; - const int term_rows = (upper_pixel_last_sixel + term->cell_height - 1) / term->cell_height; + 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); @@ -1222,6 +1327,8 @@ ? 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 */ @@ -1263,6 +1370,7 @@ 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}; @@ -1277,27 +1385,36 @@ 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) +resize_horizontally(struct terminal *term, int new_width_mutable) { - if (unlikely(new_width > term->sixel.max_width)) { + if (unlikely(new_width_mutable > term->sixel.max_width)) { LOG_WARN("maximum image dimensions exceeded, truncating"); - new_width = term->sixel.max_width; + new_width_mutable = term->sixel.max_width; } - if (unlikely(term->sixel.image.width == new_width)) + if (unlikely(term->sixel.image.width >= new_width_mutable)) return; const int sixel_row_height = 6 * term->sixel.pan; uint32_t *old_data = term->sixel.image.data; const int old_width = term->sixel.image.width; + const int 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; @@ -1307,6 +1424,7 @@ 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); @@ -1316,24 +1434,26 @@ uint32_t bg = term->sixel.default_bg; /* Copy old rows, and initialize new columns to background color */ - for (int r = 0; r < height; r++) { - memcpy(&new_data[r * new_width], - &old_data[r * old_width], - old_width * sizeof(uint32_t)); - - for (int c = old_width; c < new_width; c++) - new_data[r * new_width + c] = bg; + const uint32_t *end = &new_data[alloc_height * new_width]; + for (uint32_t *n = new_data, *o = old_data; + n < end; + n += new_width, o += old_width) + { + memcpy(n, o, old_width * sizeof(uint32_t)); + memset_u32(&n[old_width], bg, new_width - old_width); } free(old_data); term->sixel.image.data = new_data; term->sixel.image.width = new_width; - term->sixel.row_byte_ofs = term->sixel.pos.row * new_width; + + const int ofs = term->sixel.pos.row * new_width + term->sixel.pos.col; + term->sixel.image.p = &term->sixel.image.data[ofs]; } static bool -resize_vertically(struct terminal *term, int new_height) +resize_vertically(struct terminal *term, const int new_height) { LOG_DBG("resizing image vertically: (%d)x%d -> (%d)x%d", term->sixel.image.width, term->sixel.image.height, @@ -1347,14 +1467,16 @@ uint32_t *old_data = term->sixel.image.data; const int width = term->sixel.image.width; const int old_height = term->sixel.image.height; + const int sixel_row_height = 6 * term->sixel.pan; - int alloc_height = (new_height + 6 - 1) / 6 * 6; + int alloc_height = (new_height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; xassert(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; } @@ -1366,57 +1488,88 @@ return false; } - uint32_t bg = term->sixel.default_bg; + const uint32_t bg = term->sixel.default_bg; - /* Initialize new rows to background color */ - for (int r = old_height; r < new_height; r++) { - for (int c = 0; c < width; c++) - new_data[r * width + c] = bg; - } + memset_u32(&new_data[old_height * width], + bg, + (alloc_height - old_height) * width); - term->sixel.image.data = new_data; 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, int new_height) +resize(struct terminal *term, int new_width_mutable, int new_height_mutable) { LOG_DBG("resizing image: %dx%d -> %dx%d", term->sixel.image.width, term->sixel.image.height, - new_width, new_height); + new_width_mutable, new_height_mutable); - if (unlikely(new_width > term->sixel.max_width)) { + if (unlikely(new_width_mutable > term->sixel.max_width)) { LOG_WARN("maximum image width exceeded, truncating"); - new_width = term->sixel.max_width; + new_width_mutable = term->sixel.max_width; } - if (unlikely(new_height > term->sixel.max_height)) { + if (unlikely(new_height_mutable > term->sixel.max_height)) { LOG_WARN("maximum image height exceeded, truncating"); - new_height = term->sixel.max_height; + new_height_mutable = term->sixel.max_height; } + 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; - int alloc_new_width = new_width; - int alloc_new_height = (new_height + sixel_row_height - 1) / sixel_row_height * sixel_row_height; + 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; - uint32_t bg = term->sixel.default_bg; + const uint32_t bg = term->sixel.default_bg; + + /* + * If the image is resized horizontally, or if it's opaque, we + * need to explicitly initialize the "new" pixels. + * + * When the image is *not* resized horizontally, we simply do a + * realloc(). In this case, there's no need to manually copy the + * old pixels. We do however need to initialize the new pixels + * since realloc() returns uninitialized memory. + * + * When the image *is* resized horizontally, we need to allocate + * new memory (when the width changes, the stride changes, and + * thus we cannot simply realloc()) + * + * If the default background is transparent, the new pixels need + * to be initialized to 0x0. We do this by using calloc(). + * + * If the default background is opaque, then we need to manually + * initialize the new pixels. + */ + const bool initialize_bg = + !term->sixel.transparent_bg || new_width == old_width; if (new_width == old_width) { /* Width (and thus stride) is the same, so we can simply * re-alloc the existing buffer */ - new_data = realloc(old_data, alloc_new_width * alloc_new_height * sizeof(uint32_t)); + new_data = realloc(old_data, new_width * alloc_new_height * sizeof(uint32_t)); if (new_data == NULL) { LOG_ERRNO("failed to reallocate sixel image buffer"); return false; @@ -1427,81 +1580,80 @@ } else { /* Width (and thus stride) change - need to allocate a new buffer */ xassert(new_width > old_width); - new_data = xmalloc(alloc_new_width * alloc_new_height * sizeof(uint32_t)); + const size_t pixels = new_width * alloc_new_height; + + new_data = !initialize_bg + ? xcalloc(pixels, sizeof(uint32_t)) + : xmalloc(pixels * sizeof(uint32_t)); /* Copy old rows, and initialize new columns to background color */ - for (int r = 0; r < min(old_height, new_height); r++) { - memcpy(&new_data[r * new_width], &old_data[r * old_width], old_width * sizeof(uint32_t)); + const int row_copy_count = min(old_height, alloc_new_height); + const uint32_t *end = &new_data[row_copy_count * new_width]; - for (int c = old_width; c < new_width; c++) - new_data[r * new_width + c] = bg; + for (uint32_t *n = new_data, *o = old_data; + n < end; + n += new_width, o += old_width) + { + memcpy(n, o, old_width * sizeof(uint32_t)); + memset_u32(&n[old_width], bg, new_width - old_width); } free(old_data); } - /* Initialize new rows to background color */ - for (int r = old_height; r < new_height; r++) { - for (int c = 0; c < new_width; c++) - new_data[r * new_width + c] = bg; + if (initialize_bg) { + memset_u32(&new_data[old_height * new_width], + bg, + (alloc_new_height - old_height) * new_width); } xassert(new_data != NULL); term->sixel.image.data = new_data; term->sixel.image.width = new_width; term->sixel.image.height = new_height; - term->sixel.row_byte_ofs = term->sixel.pos.row * new_width; + term->sixel.image.alloc_height = alloc_new_height; + term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * new_width + term->sixel.pos.col]; return true; } static void -sixel_add_generic(struct terminal *term, int col, int width, uint32_t color, +sixel_add_generic(struct terminal *term, uint32_t *data, int stride, uint32_t color, uint8_t sixel) { - xassert(term->sixel.pos.col < term->sixel.image.width); - xassert(term->sixel.pos.row < term->sixel.image.height); - const int pan = term->sixel.pan; - size_t ofs = term->sixel.row_byte_ofs + col; - uint32_t *data = &term->sixel.image.data[ofs]; for (int i = 0; i < 6; i++, sixel >>= 1) { if (sixel & 1) { - for (int r = 0; r < pan; r++, data += width) + for (int r = 0; r < pan; r++, data += stride) *data = color; } else - data += width * pan; + data += stride * pan; } xassert(sixel == 0); } -static void -sixel_add_ar_11(struct terminal *term, int col, int width, uint32_t color, +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.pos.col < term->sixel.image.width); - xassert(term->sixel.pos.row < term->sixel.image.height); xassert(term->sixel.pan == 1); - const size_t ofs = term->sixel.row_byte_ofs + col; - uint32_t *data = &term->sixel.image.data[ofs]; - if (sixel & 0x01) *data = color; - data += width; + data += stride; if (sixel & 0x02) *data = color; - data += width; + data += stride; if (sixel & 0x04) *data = color; - data += width; + data += stride; if (sixel & 0x08) *data = color; - data += width; + data += stride; if (sixel & 0x10) *data = color; - data += width; + data += stride; if (sixel & 0x20) *data = color; } @@ -1518,15 +1670,49 @@ resize_horizontally(term, col + count); width = term->sixel.image.width; count = min(count, max(width - col, 0)); + + if (unlikely(count == 0)) + return; } uint32_t color = term->sixel.color; - for (unsigned i = 0; i < count; i++, col++) { - /* TODO: is it worth dynamically dispatching to either generic or AR-11? */ - sixel_add_generic(term, col, width, color, c); + 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; } - term->sixel.pos.col = col; + 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 @@ -1542,13 +1728,22 @@ resize_horizontally(term, col + count); width = term->sixel.image.width; count = min(count, max(width - col, 0)); + + if (unlikely(count == 0)) + return; } uint32_t color = term->sixel.color; - for (unsigned i = 0; i < count; i++, col++) - sixel_add_ar_11(term, col, width, color, c); + uint32_t *data = term->sixel.image.p; + uint32_t *end = data + count; + + term->sixel.pos.col += count; + term->sixel.image.p = end; + term->sixel.image.bottom_pixel |= c; + + for (; data < end; data++) + sixel_add_ar_11(term, data, width, color, c); - term->sixel.pos.col = col; } IGNORE_WARNING("-Wpedantic") @@ -1580,21 +1775,23 @@ case '$': if (likely(term->sixel.pos.col <= term->sixel.max_width)) { /* - * We set, and keep, ‘col’ outside the image boundary when - * we’ve reached the maximum image height, to avoid also + * We set, and keep, 'col' outside the image boundary when + * we've reached the maximum image height, to avoid also * having to check the row vs image height in the common * path in sixel_add(). */ term->sixel.pos.col = 0; + term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * term->sixel.image.width]; } break; - case '-': + case '-': /* GNL - Graphical New Line */ term->sixel.pos.row += 6 * term->sixel.pan; term->sixel.pos.col = 0; - term->sixel.row_byte_ofs += term->sixel.image.width * 6 * term->sixel.pan; + term->sixel.image.bottom_pixel = 0; + term->sixel.image.p = &term->sixel.image.data[term->sixel.pos.row * term->sixel.image.width]; - if (term->sixel.pos.row >= term->sixel.image.height) { + if (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; } @@ -1621,7 +1818,7 @@ decsixel_ar_11(struct terminal *term, uint8_t c) { if (likely(c >= '?' && c <= '~')) - sixel_add_many_ar_11(term, c - 63, 1); + sixel_add_one_ar_11(term, c - 63); else decsixel_generic(term, c); } @@ -1655,18 +1852,71 @@ 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; - term->sixel.pan = pan; - term->sixel.pad = 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); } @@ -1775,12 +2025,12 @@ int sat = min(c3, 100); /* - * Sixel’s HLS use the following primary color hues: + * Sixel's HLS use the following primary color hues: * blue: 0° * red: 120° * green: 240° * - * While “standard” HSL uses: + * While "standard" HSL uses: * red: 0° * green: 120° * blue: 240° diff -Nru foot-1.16.2/slave.c foot-1.17.2/slave.c --- foot-1.16.2/slave.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/slave.c 2024-04-17 09:26:45.000000000 +0000 @@ -21,10 +21,16 @@ #include "macros.h" #include "terminal.h" #include "tokenize.h" +#include "util.h" #include "xmalloc.h" extern char **environ; +struct environ { + size_t count; + char **envp; +}; + #if defined(__FreeBSD__) static char * find_file_in_path(const char *file) @@ -116,7 +122,7 @@ if (line[0] == '#') continue; - if (strcmp(line, shell) == 0) { + if (streq(line, shell)) { fclose(f); return true; } @@ -303,9 +309,69 @@ _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 = xasprintf("%s=%s", name, value); + + /* Search for existing variable. If found, replace it with the + new value */ + for (size_t i = 0; i < env->count; i++) { + if (env_matches_var_name(env->envp[i], name)) { + free(env->envp[i]); + env->envp[i] = e; + return; + } + } + + /* If the variable does not already exist, add it */ + env->envp = xrealloc(env->envp, (env->count + 2) * sizeof(env->envp[0])); + env->envp[env->count++] = e; + env->envp[env->count] = NULL; + } +} + +static void +del_from_env(struct environ *env, const char *name) +{ + if (env->envp == NULL) + unsetenv(name); + else { + for (size_t i = 0; i < env->count; i++) { + if (env_matches_var_name(env->envp[i], name)) { + free(env->envp[i]); + memmove(&env->envp[i], + &env->envp[i + 1], + (env->count - i) * sizeof(env->envp[0])); + env->count--; + xassert(env->envp[env->count] == NULL); + break; + } + } + } +} + pid_t slave_spawn(int ptmx, int argc, const char *cwd, char *const *argv, - char *const *envp, const env_var_list_t *extra_env_vars, + const char *const *envp, const env_var_list_t *extra_env_vars, const char *term_env, const char *conf_shell, bool login_shell, const user_notifications_t *notifications) { @@ -350,15 +416,30 @@ _exit(errno_copy); } - setenv("TERM", term_env, 1); - setenv("COLORTERM", "truecolor", 1); - setenv("PWD", cwd, 1); + /* Create a mutable copy of the environment */ + struct environ custom_env = {0}; + if (envp != NULL) { + for (const char *const *e = envp; *e != NULL; e++) + custom_env.count++; + + custom_env.envp = xcalloc( + custom_env.count + 1, sizeof(custom_env.envp[0])); + + size_t i = 0; + for (const char *const *e = envp; *e != NULL; e++, i++) + custom_env.envp[i] = xstrdup(*e); + xassert(custom_env.envp[custom_env.count] == NULL); + } + + add_to_env(&custom_env, "TERM", term_env); + add_to_env(&custom_env, "COLORTERM", "truecolor"); + add_to_env(&custom_env, "PWD", cwd); - unsetenv("TERM_PROGRAM"); - unsetenv("TERM_PROGRAM_VERSION"); + del_from_env(&custom_env, "TERM_PROGRAM"); + del_from_env(&custom_env, "TERM_PROGRAM_VERSION"); #if defined(FOOT_TERMINFO_PATH) - setenv("TERMINFO", FOOT_TERMINFO_PATH, 1); + add_to_env(&custom_env, "TERMINFO", FOOT_TERMINFO_PATH); #endif if (extra_env_vars != NULL) { @@ -367,9 +448,9 @@ const char *value = it->item.value; if (strlen(value) == 0) - unsetenv(name); + del_from_env(&custom_env, name); else - setenv(name, value, 1); + add_to_env(&custom_env, name, value); } } @@ -393,14 +474,23 @@ } if (is_valid_shell(shell_argv[0])) - setenv("SHELL", shell_argv[0], 1); + add_to_env(&custom_env, "SHELL", shell_argv[0]); - slave_exec(ptmx, shell_argv, envp != NULL ? envp : environ, + slave_exec(ptmx, shell_argv, + custom_env.envp != NULL ? custom_env.envp : environ, fork_pipe[1], login_shell, notifications); BUG("Unexpected return from slave_exec()"); break; default: { + + /* + * Don't stay in CWD, since it may be an ephemeral path. For + * example, it may be a mount point of, say, a thumb drive. Us + * keeping it open will prevent the user from unmounting it. + */ + (void)!!chdir("/"); + close(fork_pipe[1]); /* Close write end */ LOG_DBG("slave has PID %d", pid); diff -Nru foot-1.16.2/slave.h foot-1.17.2/slave.h --- foot-1.16.2/slave.h 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/slave.h 2024-04-17 09:26:45.000000000 +0000 @@ -7,7 +7,7 @@ #include "user-notification.h" pid_t slave_spawn( - int ptmx, int argc, const char *cwd, char *const *argv, char *const *envp, + int ptmx, int argc, const char *cwd, char *const *argv, const char *const *envp, const env_var_list_t *extra_env_vars, const char *term_env, const char *conf_shell, bool login_shell, const user_notifications_t *notifications); diff -Nru foot-1.16.2/spawn.c foot-1.17.2/spawn.c --- foot-1.16.2/spawn.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/spawn.c 2024-04-17 09:26:45.000000000 +0000 @@ -145,7 +145,7 @@ expanded[len] = '\0'; \ } while (0) - *argv = malloc((*argc + 1) * sizeof((*argv)[0])); + *argv = xmalloc((*argc + 1) * sizeof((*argv)[0])); /* Expand the provided keys */ for (size_t i = 0; i < *argc; i++) { diff -Nru foot-1.16.2/terminal.c foot-1.17.2/terminal.c --- foot-1.16.2/terminal.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/terminal.c 2024-04-17 09:26:45.000000000 +0000 @@ -51,11 +51,8 @@ enqueue_data_for_slave(const void *data, size_t len, size_t offset, ptmx_buffer_list_t *buffer_list) { - void *copy = xmalloc(len); - memcpy(copy, data, len); - struct ptmx_buffer queued = { - .data = copy, + .data = xmemdup(data, len), .len = len, .idx = offset, }; @@ -260,8 +257,8 @@ if (unlikely(term->interactive_resizing.grid != NULL)) { /* - * Don’t consume PTMX while we’re doing an interactive resize, - * since the ‘normal’ grid we’re currently using is a + * Don't consume PTMX while we're doing an interactive resize, + * since the 'normal' grid we're currently using is a * temporary one - all changes done to it will be lost when * the interactive resize ends. */ @@ -367,6 +364,20 @@ 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; @@ -622,13 +633,36 @@ struct itimerspec reset = {{0}}; timerfd_settime(term->render.title.timer_fd, 0, &reset, NULL); - term->render.title.is_armed = false; render_refresh_title(term); return true; } static bool +fdm_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); @@ -784,10 +818,11 @@ * render_resize() after this function */ if (resize_grid) { /* Use force, since cell-width/height may have changed */ - render_resize_force( + render_resize( term, (int)roundf(term->width / term->scale), - (int)roundf(term->height / term->scale)); + (int)roundf(term->height / term->scale), + RESIZE_FORCE | RESIZE_KEEP_GRID); } return true; } @@ -816,16 +851,32 @@ * 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 + * monitor's real DPI, since we scale everything to the correct * scaling factor (no downscaling done by the compositor). */ xassert(tll_length(term->wl->monitors) > 0); const struct wl_window *win = term->window; - const struct monitor *mon = tll_length(win->on_outputs) > 0 - ? tll_back(win->on_outputs) - : &tll_front(term->wl->monitors); + 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); + } if (term_fractional_scaling(term)) return mon != NULL ? mon->dpi.physical : 96.; @@ -939,11 +990,7 @@ snprintf(size, sizeof(size), ":size=%.2f", term->font_sizes[i][j].pt_size * scale); - size_t len = strlen(font->pattern) + strlen(size) + 1; - names[i][j] = xmalloc(len); - - strcpy(names[i][j], font->pattern); - strcat(names[i][j], size); + names[i][j] = xstrjoin(font->pattern, size); } } @@ -966,30 +1013,14 @@ const char **names_bold_italic = (const char **)(custom_bold_italic ? names[3] : names[0]); const bool use_dpi = term->font_is_sized_by_dpi; + char *dpi = xasprintf("dpi=%.2f", use_dpi ? term->font_dpi : 96.); - char *attrs[4] = {NULL}; - int attr_len[4] = {-1, -1, -1, -1}; /* -1, so that +1 (below) results in 0 */ - - for (size_t i = 0; i < 2; i++) { - attr_len[0] = snprintf( - attrs[0], attr_len[0] + 1, "dpi=%.2f", - use_dpi ? term->font_dpi : 96); - attr_len[1] = snprintf( - attrs[1], attr_len[1] + 1, "dpi=%.2f:%s", - use_dpi ? term->font_dpi : 96, !custom_bold ? "weight=bold" : ""); - attr_len[2] = snprintf( - attrs[2], attr_len[2] + 1, "dpi=%.2f:%s", - use_dpi ? term->font_dpi : 96, !custom_italic ? "slant=italic" : ""); - attr_len[3] = snprintf( - attrs[3], attr_len[3] + 1, "dpi=%.2f:%s", - use_dpi ? term->font_dpi : 96, !custom_bold_italic ? "weight=bold:slant=italic" : ""); - - if (i > 0) - continue; - - for (size_t i = 0; i < 4; i++) - attrs[i] = xmalloc(attr_len[i] + 1); - } + 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 *fonts[4]; struct font_load_data data[4] = { @@ -1058,10 +1089,13 @@ static void fdm_client_terminated( struct reaper *reaper, pid_t pid, int status, void *data); +static const int PTY_OPEN_FLAGS = O_RDWR | O_NOCTTY; + struct terminal * term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl, const char *foot_exe, const char *cwd, - const char *token, int argc, char *const *argv, char *const *envp, + const char *token, const char *pty_path, + int argc, char *const *argv, const char *const *envp, void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data) { int ptmx = -1; @@ -1070,6 +1104,7 @@ int delay_upper_fd = -1; int app_sync_updates_fd = -1; int title_update_fd = -1; + int app_id_update_fd = -1; struct terminal *term = malloc(sizeof(*term)); if (unlikely(term == NULL)) { @@ -1077,7 +1112,8 @@ return NULL; } - if ((ptmx = posix_openpt(O_RDWR | O_NOCTTY)) < 0) { + ptmx = pty_path ? open(pty_path, PTY_OPEN_FLAGS) : posix_openpt(PTY_OPEN_FLAGS); + if (ptmx < 0) { LOG_ERRNO("failed to open PTY"); goto close_fds; } @@ -1104,6 +1140,12 @@ 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) { @@ -1111,8 +1153,8 @@ goto close_fds; } - /* Need to register *very* early (before the first “goto err”), to - * ensure term_destroy() doesn’t unref a key-binding we haven’t + /* Need to register *very* early (before the first "goto err"), to + * ensure term_destroy() doesn't unref a key-binding we haven't * yet ref:d */ key_binding_new_for_conf(wayl->key_binding_manager, wayl, conf); @@ -1134,7 +1176,8 @@ !fdm_add(fdm, delay_lower_fd, EPOLLIN, &fdm_delayed_render, term) || !fdm_add(fdm, delay_upper_fd, EPOLLIN, &fdm_delayed_render, term) || !fdm_add(fdm, app_sync_updates_fd, EPOLLIN, &fdm_app_sync_updates_timeout, term) || - !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term)) + !fdm_add(fdm, title_update_fd, EPOLLIN, &fdm_title_update_timeout, term) || + !fdm_add(fdm, app_id_update_fd, EPOLLIN, &fdm_app_id_update_timeout, term)) { goto err; } @@ -1144,6 +1187,7 @@ .fdm = fdm, .reaper = reaper, .conf = conf, + .slave = -1, .ptmx = ptmx, .ptmx_buffers = tll_init(), .ptmx_paste_buffers = tll_init(), @@ -1154,6 +1198,7 @@ xmalloc(sizeof(term->font_sizes[3][0]) * conf->fonts[3].count), }, .font_dpi = 0., + .font_dpi_before_unmap = -1., .font_subpixel = (conf->colors.alpha == 0xffff /* Can't do subpixel rendering on transparent background */ ? FCFT_SUBPIXEL_DEFAULT : FCFT_SUBPIXEL_NONE), @@ -1228,9 +1273,11 @@ .scrollback_lines = conf->scrollback.lines, .app_sync_updates.timer_fd = app_sync_updates_fd, .title = { - .is_armed = false, .timer_fd = title_update_fd, }, + .app_id = { + .timer_fd = app_id_update_fd, + }, .workers = { .count = conf->render_worker_count, .queue = tll_init(), @@ -1276,16 +1323,18 @@ add_utmp_record(conf, reaper, ptmx); - /* Start the slave/client */ - if ((term->slave = slave_spawn( - term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, - conf->term, conf->shell, conf->login_shell, - &conf->notifications)) == -1) - { - goto err; - } + if (!pty_path) { + /* Start the slave/client */ + if ((term->slave = slave_spawn( + term->ptmx, argc, term->cwd, argv, envp, &conf->env_vars, + conf->term, conf->shell, conf->login_shell, + &conf->notifications)) == -1) + { + goto err; + } - reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); + reaper_add(term->reaper, term->slave, &fdm_client_terminated, term); + } /* Guess scale; we're not mapped yet, so we don't know on which * output we'll be. Use scaling factor from first monitor */ @@ -1339,6 +1388,7 @@ fdm_del(fdm, delay_upper_fd); fdm_del(fdm, app_sync_updates_fd); fdm_del(fdm, title_update_fd); + fdm_del(fdm, app_id_update_fd); free(term); return NULL; @@ -1359,11 +1409,11 @@ * * A foot instance can be terminated in two ways: * - * - the client application terminates (user types ‘exit’, or pressed C-d in the + * - the client application terminates (user types 'exit', or pressed C-d in the * shell, etc) * - the foot window is closed * - * Both variants need to trigger to “other” action. I.e. if the client + * Both variants need to trigger to "other" action. I.e. if the client * application is terminated, then we need to close the window. If the window is * closed, we need to terminate the client application. * @@ -1380,7 +1430,7 @@ * - fdm_client_terminated(): reaper callback, called when the client * application has terminated. * - * + Kills the “terminate” timeout timer + * + Kills the "terminate" timeout timer * + Calls shutdown_maybe_done() if the shutdown procedure has already * started (i.e. the window being closed initiated the shutdown) * -OR- @@ -1388,18 +1438,18 @@ * application termination initiated the shutdown). * * - term_shutdown(): unregisters all FDM callbacks, sends SIGTERM to the client - * application and installs a “terminate” timeout timer (if it hasn’t already + * application and installs a "terminate" timeout timer (if it hasn't already * terminated). Finally registers an event FD with the FDM, which is * immediately triggered. This is done to ensure any pending FDM events are * handled before shutting down. * * - fdm_shutdown(): FDM callback, triggered by the event FD in * term_shutdown(). Unmaps and destroys the window resources, and ensures the - * seats’ focused pointers don’t reference us. Finally calls + * seats' focused pointers don't reference us. Finally calls * shutdown_maybe_done(). * - * - fdm_terminate_timeout(): FDM callback for the “terminate” timeout - * timer. This function is called when the client application hasn’t + * - fdm_terminate_timeout(): FDM callback for the "terminate" timeout + * timer. This function is called when the client application hasn't * terminated after 60 seconds (after the SIGTERM). Sends SIGKILL to the * client application. * @@ -1410,7 +1460,7 @@ * It may however also be called without term_shutdown() having been called * (typically in error code paths - for example, when the Wayland connection * is closed by the compositor). In this case, the client application is - * typically still running, and we can’t assume the FDM is running. To handle + * typically still running, and we can't assume the FDM is running. To handle * this, we install configure a 60 second SIGALRM, send SIGTERM to the client * application, and then enter a blocking waitpid(). * @@ -1506,10 +1556,36 @@ struct terminal *term = data; xassert(!term->shutdown.client_has_terminated); - LOG_DBG("slave (PID=%u) has not terminated, sending SIGKILL (%d)", - term->slave, SIGKILL); + LOG_DBG("slave (PID=%u) has not terminated, sending %s (%d)", + term->slave, + term->shutdown.next_signal == SIGTERM ? "SIGTERM" + : term->shutdown.next_signal == SIGKILL ? "SIGKILL" + : "", + term->shutdown.next_signal); + + kill(-term->slave, term->shutdown.next_signal); + + switch (term->shutdown.next_signal) { + case SIGTERM: + term->shutdown.next_signal = SIGKILL; + break; + + case SIGKILL: + /* Disarm. Shouldn't be necessary, as we should be able to + shutdown completely after sending SIGKILL, before the next + timeout occurs). But lets play it safe... */ + if (term->shutdown.terminate_timeout_fd >= 0) { + timerfd_settime( + term->shutdown.terminate_timeout_fd, 0, + &(const struct itimerspec){0}, NULL); + } + break; + + default: + BUG("can only handle SIGTERM and SIGKILL"); + return false; + } - kill(-term->slave, SIGKILL); return true; } @@ -1531,6 +1607,7 @@ 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.title.timer_fd); fdm_del(term->fdm, term->delayed_render_timer.lower_fd); fdm_del(term->fdm, term->delayed_render_timer.upper_fd); @@ -1545,30 +1622,43 @@ close(term->ptmx); if (!term->shutdown.client_has_terminated) { - LOG_DBG("initiating asynchronous terminate of slave; " - "sending SIGTERM to PID=%u", term->slave); + if (term->slave <= 0) { + term->shutdown.client_has_terminated = true; + } else { + LOG_DBG("initiating asynchronous terminate of slave; " + "sending SIGHUP to PID=%u", term->slave); - kill(-term->slave, SIGTERM); + kill(-term->slave, SIGHUP); + + /* + * 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}}; - const struct itimerspec timeout = {.it_value = {.tv_sec = 60}}; + int timeout_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); + if (timeout_fd < 0 || + timerfd_settime(timeout_fd, 0, &timeout, NULL) < 0 || + !fdm_add(term->fdm, timeout_fd, EPOLLIN, &fdm_terminate_timeout, term)) + { + if (timeout_fd >= 0) + close(timeout_fd); + LOG_ERRNO("failed to create slave terminate timeout FD"); + return false; + } - int timeout_fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); - if (timeout_fd < 0 || - timerfd_settime(timeout_fd, 0, &timeout, NULL) < 0 || - !fdm_add(term->fdm, timeout_fd, EPOLLIN, &fdm_terminate_timeout, term)) - { - if (timeout_fd >= 0) - close(timeout_fd); - LOG_ERRNO("failed to create slave terminate timeout FD"); - return false; + xassert(term->shutdown.terminate_timeout_fd < 0); + term->shutdown.terminate_timeout_fd = timeout_fd; + term->shutdown.next_signal = SIGTERM; } - - xassert(term->shutdown.terminate_timeout_fd < 0); - term->shutdown.terminate_timeout_fd = timeout_fd; } term->selection.auto_scroll.fd = -1; term->render.app_sync_updates.timer_fd = -1; + term->render.app_id.timer_fd = -1; term->render.title.timer_fd = -1; term->delayed_render_timer.lower_fd = -1; term->delayed_render_timer.upper_fd = -1; @@ -1622,6 +1712,7 @@ 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.title.timer_fd); fdm_del(term->fdm, term->delayed_render_timer.lower_fd); fdm_del(term->fdm, term->delayed_render_timer.upper_fd); @@ -1665,6 +1756,7 @@ composed_free(term->composed); + free(term->app_id); free(term->window_title); tll_free_and_free(term->window_title_stack, free); @@ -1734,7 +1826,7 @@ int ret = EXIT_SUCCESS; if (term->slave > 0) { - /* We’ll deal with this explicitly */ + /* We'll deal with this explicitly */ reaper_del(term->reaper, term->slave); int exit_status; @@ -1743,12 +1835,12 @@ exit_status = term->shutdown.exit_status; else { LOG_DBG("initiating blocking terminate of slave; " - "sending SIGTERM to PID=%u", term->slave); + "sending SIGHUP to PID=%u", term->slave); - kill(-term->slave, SIGTERM); + kill(-term->slave, SIGHUP); /* - * we’ve closed the ptxm, and sent SIGTERM to the client + * we've closed the ptxm, and sent SIGTERM to the client * application. It *should* exit... * * But, since it is possible to write clients that ignore @@ -1766,7 +1858,12 @@ struct sigaction action = {.sa_handler = &sig_alarm}; sigemptyset(&action.sa_mask); sigaction(SIGALRM, &action, NULL); - alarm(60); + + /* Wait, then send SIGTERM, wait again, then send SIGKILL */ + int next_signal = SIGTERM; + + alarm_raised = 0; + alarm(30); while (true) { int r = waitpid(term->slave, &exit_status, 0); @@ -1778,11 +1875,16 @@ xassert(errno == EINTR); if (alarm_raised) { - LOG_DBG( - "slave (PID=%u) has not terminate yet, " - "sending: SIGKILL (%d)", term->slave, SIGKILL); + LOG_DBG("slave (PID=%u) has not terminated yet, " + "sending: %s (%d)", term->slave, + next_signal == SIGTERM ? "SIGTERM" : "SIGKILL", + next_signal); + + kill(-term->slave, next_signal); + next_signal = SIGKILL; - kill(-term->slave, SIGKILL); + alarm_raised = 0; + alarm(30); } } } @@ -1844,7 +1946,9 @@ { erase_cell_range(term, row, 0, term->cols - 1); row->linebreak = false; - row->prompt_marker = false; + row->shell_integration.prompt_marker = false; + row->shell_integration.cmd_start = -1; + row->shell_integration.cmd_end = -1; } void @@ -2092,31 +2196,42 @@ } 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 + * 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 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 + * - if there aren't any outputs available, use 1.0 */ const float new_scale = (term_fractional_scaling(term) ? win->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.); + : 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; @@ -2152,6 +2267,7 @@ } term->font_dpi = dpi; + term->font_dpi_before_unmap = dpi; term->font_is_sized_by_dpi = will_scale_using_dpi; if (!need_font_reload) @@ -2266,7 +2382,7 @@ dmg->region.start == region.start && dmg->region.end == region.end)) { - /* Make sure we don’t overflow... */ + /* Make sure we don't overflow... */ int new_line_count = (int)dmg->lines + lines; if (likely(new_line_count <= UINT16_MAX)) { dmg->lines = new_line_count; @@ -2318,6 +2434,10 @@ 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; @@ -2330,14 +2450,14 @@ if (sel_end >= 0) { /* * Cancel selection if it touches any of the rows in the - * scrollback, since we can’t have the selection reference + * scrollback, since we can't have the selection reference * soon-to-be deleted rows. * * This is done by range checking the selection range against * the scrollback range. * * To make this comparison simpler, the start/end absolute row - * numbers are “rebased” against the scrollback start, where + * numbers are "rebased" against the scrollback start, where * row 0 is the *first* row in the scrollback. A high number * thus means the row is further *down* in the scrollback, * closer to the screen bottom. @@ -2384,6 +2504,13 @@ } 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); } @@ -3047,7 +3174,7 @@ */ xkb_mod_mask_t mods; - get_current_modifiers(seat, &mods, NULL, 0); + get_current_modifiers(seat, &mods, NULL, 0, true); const struct key_binding_set *bindings = key_binding_for(term->wl->key_binding_manager, term->conf, seat); @@ -3252,9 +3379,16 @@ if (term->conf->locked_title && term->window_title_has_been_set) return; - if (term->window_title != NULL && strcmp(term->window_title, title) == 0) + if (term->window_title != NULL && streq(term->window_title, title)) return; + if (mbsntoc32(NULL, title, strlen(title), 0) == (char32_t)-1) { + /* 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); @@ -3262,6 +3396,25 @@ } 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 && strcmp(term->app_id, app_id) == 0) + return; + + free(term->app_id); + if (app_id != NULL) { + term->app_id = xstrdup(app_id); + } else { + term->app_id = NULL; + } + render_refresh_app_id(term); +} + +void term_flash(struct terminal *term, unsigned duration_ms) { LOG_DBG("FLASH for %ums", duration_ms); @@ -3288,7 +3441,7 @@ if (!wayl_win_set_urgent(term->window)) { /* * Urgency (xdg-activation) is relatively new in - * Wayland. Fallback to our old, “faked”, urgency - + * Wayland. Fallback to our old, "faked", urgency - * rendering our window margins in red */ term->render.urgency = true; @@ -3429,6 +3582,54 @@ cell->attrs = term->vt.attrs; } +/* + * 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; + + if (unlikely(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(row->extra != NULL)) + grid_row_uri_range_erase(row, c, c + count - 1); +} + void term_print(struct terminal *term, char32_t wc, int width) { @@ -3497,11 +3698,13 @@ grid_row_uri_range_erase(row, col, col + width - 1); /* Advance cursor the 'additional' columns while dirty:ing the cells */ - for (int i = 1; i < width && col < term->cols - 1; i++) { + for (int i = 1; i < width && (col + 1) < term->cols; i++) { col++; print_spacer(term, col, width - i); } + xassert(col < term->cols); + /* Advance cursor */ if (unlikely(++col >= term->cols)) { grid->cursor.lcf = true; @@ -3543,6 +3746,7 @@ /* Advance cursor */ if (unlikely(++col >= term->cols)) { + xassert(col == term->cols); grid->cursor.lcf = true; col--; } else @@ -3619,7 +3823,7 @@ static bool rows_to_text(const struct terminal *term, int start, int end, - char **text, size_t *len) + int col_start, int col_end, char **text, size_t *len) { struct extraction_context *ctx = extract_begin(SELECTION_NONE, true); if (ctx == NULL) @@ -3632,15 +3836,20 @@ const struct row *row = term->grid->rows[r]; xassert(row != NULL); - for (int c = 0; c < term->cols; c++) + const int c_end = r == end ? col_end : term->cols; + + for (int c = col_start; c < c_end; c++) { if (!extract_one(term, row, &row->cells[c], c, ctx)) goto out; + } if (r == end) break; r++; r &= grid_rows - 1; + + col_start = 0; } out: @@ -3672,7 +3881,7 @@ end += term->grid->num_rows; } - return rows_to_text(term, start, end, text, len); + return rows_to_text(term, start, end, 0, term->cols, text, len); } bool @@ -3680,7 +3889,91 @@ { int start = grid_row_absolute_in_view(term->grid, 0); int end = grid_row_absolute_in_view(term->grid, term->rows - 1); - return rows_to_text(term, start, end, text, len); + return rows_to_text(term, start, end, 0, term->cols, text, len); +} + +bool +term_command_output_to_text(const struct terminal *term, char **text, size_t *len) +{ + int start_row = -1; + int end_row = -1; + int start_col = -1; + int end_col = -1; + + const struct grid *grid = term->grid; + const int sb_end = grid_row_absolute(grid, term->rows - 1); + const int sb_start = (sb_end + 1) & (grid->num_rows - 1); + int r = sb_end; + + while (start_row < 0) { + const struct row *row = grid->rows[r]; + if (row == NULL) + break; + + if (row->shell_integration.cmd_end >= 0) { + end_row = r; + end_col = row->shell_integration.cmd_end; + } + + if (end_row >= 0 && row->shell_integration.cmd_start >= 0) { + start_row = r; + start_col = row->shell_integration.cmd_start; + } + + if (r == sb_start) + break; + + r = (r - 1 + grid->num_rows) & (grid->num_rows - 1); + } + + if (start_row < 0) + return false; + + bool ret = rows_to_text(term, start_row, end_row, start_col, end_col, text, len); + if (!ret) + return false; + + /* + * If the FTCS_COMMAND_FINISHED marker was emitted at the *first* + * column, then the *entire* previous line is part of the command + * output. *Including* the newline, if any. + * + * Since rows_to_text() doesn't extract the column + * FTCS_COMMAND_FINISHED was emitted at (that would be wrong - + * FTCS_COMMAND_FINISHED is emitted *after* the command output, + * not at its last character), the extraction logic will not see + * the last newline (this is true for all non-line-wise selection + * types), and the extracted text will *not* end with a newline. + * + * Here we try to compensate for that. Note that if 'end_col' is + * not 0, then the command output only covers a partial row, and + * thus we do *not* want to append a newline. + */ + + if (end_col > 0) { + /* Command output covers partial row - don't append newline */ + return true; + } + + int next_to_last_row = (end_row - 1 + grid->num_rows) & (grid->num_rows - 1); + const struct row *row = grid->rows[next_to_last_row]; + + /* Add newline if last row has a hard linebreak */ + if (row->linebreak) { + char *new_text = xrealloc(*text, *len + 1 + 1); + + if (new_text == NULL) { + /* Ignore failure - use text as is (without inserting newline) */ + return true; + } + + *text = new_text; + (*len)++; + (*text)[*len - 1] = '\n'; + (*text)[*len] = '\0'; + } + + return true; } bool diff -Nru foot-1.16.2/terminal.h foot-1.17.2/terminal.h --- foot-1.16.2/terminal.h 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/terminal.h 2024-04-17 09:26:45.000000000 +0000 @@ -121,8 +121,11 @@ bool dirty; bool linebreak; - /* Shell integration */ - bool prompt_marker; + struct { + bool prompt_marker; + int cmd_start; /* Column, -1 if unset */ + int cmd_end; /* Column, -1 if unset */ + } shell_integration; }; struct sixel { @@ -149,8 +152,8 @@ * 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’. + * to ensure it stays within its cell boundaries. 'scaled' is a + * cached, rescaled version of 'data' + 'pix'. */ int cell_width; int cell_height; @@ -343,7 +346,7 @@ char32_t *key; struct range range; enum url_action action; - bool url_mode_dont_change_url_attr; /* Entering/exiting URL mode doesn’t touch the cells’ attr.url */ + bool url_mode_dont_change_url_attr; /* Entering/exiting URL mode doesn't touch the cells' attr.url */ bool osc8; bool duplicate; }; @@ -379,7 +382,7 @@ bool bracketed_paste; bool focus_events; bool alt_scrolling; - bool modify_other_keys_2; /* True when modifyOtherKeys=2 (i.e. “CSI >4;2m”) */ + bool modify_other_keys_2; /* True when modifyOtherKeys=2 (i.e. "CSI >4;2m") */ enum cursor_origin origin; enum cursor_keys cursor_keys_mode; enum keypad_keys keypad_keys_mode; @@ -403,6 +406,7 @@ 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; @@ -480,6 +484,7 @@ bool window_title_has_been_set; char *window_title; tll(char *) window_title_stack; + char *app_id; struct { bool active; @@ -600,10 +605,14 @@ struct { struct timespec last_update; - bool is_armed; int timer_fd; } title; + 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 { @@ -641,7 +650,7 @@ } render; struct { - struct grid *grid; /* Original ‘normal’ grid, before resize started */ + struct grid *grid; /* Original 'normal' grid, before resize started */ int old_screen_rows; /* term->rows before resize started */ int old_cols; /* term->cols before resize started */ int old_hide_cursor; /* term->hide_cursor before resize started */ @@ -658,7 +667,6 @@ } state; struct coord pos; /* Current sixel coordinate */ - size_t row_byte_ofs; /* Byte position into image, for current row */ int color_idx; /* Current palette index */ uint32_t *private_palette; /* Private palette, used when private mode 1070 is enabled */ uint32_t *shared_palette; /* Shared palette, used when private mode 1070 is disabled */ @@ -667,15 +675,18 @@ 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 + * pan/pad is the sixel's aspect ratio */ int pan; int pad; @@ -713,6 +724,7 @@ bool client_has_terminated; int terminate_timeout_fd; int exit_status; + int next_signal; void (*cb)(void *data, int exit_code); void *cb_data; @@ -728,7 +740,8 @@ struct terminal *term_init( const struct config *conf, struct fdm *fdm, struct reaper *reaper, struct wayland *wayl, const char *foot_exe, const char *cwd, - const char *token, int argc, char *const *argv, char *const *envp, + const char *token, const char *pty_path, + int argc, char *const *argv, const char *const *envp, void (*shutdown_cb)(void *data, int exit_code), void *shutdown_data); bool term_shutdown(struct terminal *term); @@ -743,6 +756,7 @@ 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); @@ -789,6 +803,8 @@ void term_cursor_blink_update(struct terminal *term); void term_print(struct terminal *term, char32_t wc, int width); +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); @@ -829,6 +845,7 @@ 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); void term_flash(struct terminal *term, unsigned duration_ms); void term_bell(struct terminal *term); bool term_spawn_new(const struct terminal *term); @@ -843,6 +860,8 @@ 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); diff -Nru foot-1.16.2/tests/test-config.c foot-1.17.2/tests/test-config.c --- foot-1.16.2/tests/test-config.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/tests/test-config.c 2024-04-17 09:26:45.000000000 +0000 @@ -60,7 +60,7 @@ BUG("[%s].%s=%s: failed to parse", ctx->section, ctx->key, ctx->value); } - if (strcmp(*ptr, input[i].value) != 0) { + if (!streq(*ptr, input[i].value)) { BUG("[%s].%s=%s: set value (%s) not the expected one (%s)", ctx->section, ctx->key, ctx->value, *ptr, input[i].value); @@ -357,9 +357,7 @@ BUG("[%s].%s=%s: argv is NULL", ctx->section, ctx->key, ctx->value); for (size_t i = 0; i < ALEN(args); i++) { - if (ptr->argv.args[i] == NULL || - strcmp(ptr->argv.args[i], args[i]) != 0) - { + if (ptr->argv.args[i] == NULL || !streq(ptr->argv.args[i], args[i])) { BUG("[%s].%s=%s: set value not the expected one: " "mismatch of arg #%zu: expected=\"%s\", got=\"%s\"", ctx->section, ctx->key, ctx->value, i, @@ -426,6 +424,10 @@ {"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}, }; @@ -633,6 +635,12 @@ (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); test_pt_or_px(&ctx, &parse_section_cursor, "beam-thickness", &conf.cursor.beam_thickness); @@ -785,6 +793,17 @@ 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, @@ -875,7 +894,7 @@ for (size_t i = 0; i < ALEN(args); i++) { if (binding->aux.pipe.args[i] == NULL || - strcmp(binding->aux.pipe.args[i], args[i]) != 0) + !streq(binding->aux.pipe.args[i], args[i])) { BUG("[%s].%s=%s: pipe argv not the expected one: " "mismatch of arg #%zu: expected=\"%s\", got=\"%s\"", @@ -902,17 +921,19 @@ ctx->section, ctx->key, ctx->value, binding->action, action); } - if (binding->modifiers.ctrl != ctrl || - binding->modifiers.alt != alt || - binding->modifiers.shift != shift || - binding->modifiers.super != super) + bool have_ctrl = have_modifier(&binding->modifiers, XKB_MOD_NAME_CTRL); + bool have_alt = have_modifier(&binding->modifiers, XKB_MOD_NAME_ALT); + bool have_shift = have_modifier(&binding->modifiers, XKB_MOD_NAME_SHIFT); + bool have_super = have_modifier(&binding->modifiers, XKB_MOD_NAME_LOGO); + + if (have_ctrl != ctrl || have_alt != alt || + have_shift != shift || have_super != super) { BUG("[%s].%s=%s: modifier mismatch:\n" " have: ctrl=%d, alt=%d, shift=%d, super=%d\n" " expected: ctrl=%d, alt=%d, shift=%d, super=%d", ctx->section, ctx->key, ctx->value, - binding->modifiers.ctrl, binding->modifiers.alt, - binding->modifiers.shift, binding->modifiers.super, + have_ctrl, have_alt, have_shift, have_super, ctrl, alt, shift, super); } @@ -968,14 +989,17 @@ bindings.arr[0] = (struct config_key_binding){ .action = (test_mode == FAIL_DIFFERENT_ACTION ? max_action - 1 : max_action), - .modifiers = {.ctrl = true}, + .modifiers = tll_init(), .path = "unittest", }; + tll_push_back(bindings.arr[0].modifiers, xstrdup(XKB_MOD_NAME_CTRL)); + bindings.arr[1] = (struct config_key_binding){ .action = max_action, - .modifiers = {.ctrl = true}, + .modifiers = tll_init(), .path = "unittest", }; + tll_push_back(bindings.arr[1].modifiers, xstrdup(XKB_MOD_NAME_CTRL)); switch (type) { case KEY_BINDING: @@ -996,7 +1020,8 @@ break; case FAIL_MOUSE_OVERRIDE: - ctx->conf->mouse.selection_override_modifiers.ctrl = true; + tll_free_and_free(ctx->conf->mouse.selection_override_modifiers, free); + tll_push_back(ctx->conf->mouse.selection_override_modifiers, xstrdup(XKB_MOD_NAME_CTRL)); break; case FAIL_DIFFERENT_ARGV: @@ -1235,10 +1260,13 @@ 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); } @@ -1254,26 +1282,26 @@ ctx.value = "bar"; xassert(parse_section_environment(&ctx)); xassert(tll_length(conf.env_vars) == 1); - xassert(strcmp(tll_front(conf.env_vars).name, "FOO") == 0); - xassert(strcmp(tll_front(conf.env_vars).value, "bar") == 0); + xassert(streq(tll_front(conf.env_vars).name, "FOO")); + xassert(streq(tll_front(conf.env_vars).value, "bar")); /* Add a second variable */ ctx.key = "BAR"; ctx.value = "123"; xassert(parse_section_environment(&ctx)); xassert(tll_length(conf.env_vars) == 2); - xassert(strcmp(tll_back(conf.env_vars).name, "BAR") == 0); - xassert(strcmp(tll_back(conf.env_vars).value, "123") == 0); + xassert(streq(tll_back(conf.env_vars).name, "BAR")); + xassert(streq(tll_back(conf.env_vars).value, "123")); /* Replace the *value* of the first variable */ ctx.key = "FOO"; ctx.value = "456"; xassert(parse_section_environment(&ctx)); xassert(tll_length(conf.env_vars) == 2); - xassert(strcmp(tll_front(conf.env_vars).name, "FOO") == 0); - xassert(strcmp(tll_front(conf.env_vars).value, "456") == 0); - xassert(strcmp(tll_back(conf.env_vars).name, "BAR") == 0); - xassert(strcmp(tll_back(conf.env_vars).value, "123") == 0); + xassert(streq(tll_front(conf.env_vars).name, "FOO")); + xassert(streq(tll_front(conf.env_vars).value, "456")); + xassert(streq(tll_back(conf.env_vars).name, "BAR")); + xassert(streq(tll_back(conf.env_vars).value, "123")); config_free(&conf); } diff -Nru foot-1.16.2/themes/dracula-iterm foot-1.17.2/themes/dracula-iterm --- foot-1.16.2/themes/dracula-iterm 1970-01-01 00:00:00.000000000 +0000 +++ foot-1.17.2/themes/dracula-iterm 2024-04-17 09:26:45.000000000 +0000 @@ -0,0 +1,25 @@ +# -*- conf -*- +# Dracula iTerm2 variant + +[cursor] +color=ffffff bbbbbb + +[colors] +foreground=f8f8f2 +background=1e1f29 +regular0=000000 # black +regular1=ff5555 # red +regular2=50fa7b # green +regular3=f1fa8c # yellow +regular4=bd93f9 # blue +regular5=ff79c6 # magenta +regular6=8be9fd # cyan +regular7=bbbbbb # white +bright0=555555 # bright black +bright1=ff5555 # bright red +bright2=50fa7b # bright green +bright3=f1fa8c # bright yellow +bright4=bd93f9 # bright blue +bright5=ff79c6 # bright magenta +bright6=8be9fd # bright cyan +bright7=ffffff # bright white diff -Nru foot-1.16.2/themes/electrophoretic foot-1.17.2/themes/electrophoretic --- foot-1.16.2/themes/electrophoretic 1970-01-01 00:00:00.000000000 +0000 +++ foot-1.17.2/themes/electrophoretic 2024-04-17 09:26:45.000000000 +0000 @@ -0,0 +1,35 @@ +# -*- conf -*- +# Electrophoretic +# Theme for electrophoretic displays (like e-ink) which usually supports +# 16 levels of grays. This theme aims to maximize the contrast between the +# text and the white background. +# author: Eugen Rahaian + +[cursor] +color=ffffff 515151 + +[colors] +background= ffffff +foreground= 000000 + +# The colors are sorted based on their luminance, so we can more easily assign +# them a gray level. +# grayscale order: black_0 blue_4 red_1 magenta_5 green_2 cyan_6 yellow_3 white_7 +regular0= ffffff +regular4= 616161 +regular1= 515151 +regular5= 414141 +regular2= 313131 +regular6= 212121 +regular3= 111111 +regular7= 000000 +# Here, we also stay away from the white background by reusing the dark gray levels +# from above, with small variations +bright0= 818181 +bright4= 717171 +bright1= 616161 +bright5= 515151 +bright2= 414141 +bright6= 313131 +bright3= 212121 +bright7= 111111 diff -Nru foot-1.16.2/themes/neon foot-1.17.2/themes/neon --- foot-1.16.2/themes/neon 1970-01-01 00:00:00.000000000 +0000 +++ foot-1.17.2/themes/neon 2024-04-17 09:26:45.000000000 +0000 @@ -0,0 +1,27 @@ +# +# vim: ft=dosini +# +# Neon +# +# https://xcolors.net/neon +# + +[colors] +foreground=f8f8f8 +background=171717 +regular0=171717 +regular1=d81765 +regular2=97d01a +regular3=ffa800 +regular4=16b1fb +regular5=ff2491 +regular6=0fdcb6 +regular7=ebebeb +bright0=38252c +bright1=ff0000 +bright2=76b639 +bright3=e1a126 +bright4=289cd5 +bright5=ff2491 +bright6=0a9b81 +bright7=f8f8f8 diff -Nru foot-1.16.2/themes/noirblaze foot-1.17.2/themes/noirblaze --- foot-1.16.2/themes/noirblaze 1970-01-01 00:00:00.000000000 +0000 +++ foot-1.17.2/themes/noirblaze 2024-04-17 09:26:45.000000000 +0000 @@ -0,0 +1,31 @@ +# -*- conf -*- +# noirblaze-kitty +# https://github.com/n1ghtmare/noirblaze-kitty + + +[cursor] +color=121212 ff0088 + +[colors] +foreground=d5d5d5 +background=121212 + +# selection-foreground=121212 +# selection-background=b0b0b0 + +regular0=121212 # black +regular1=ff0088 # red +regular2=00ff77 # green +regular3=ffffff # yellow +regular4=b0b0b0 # blue +regular5=7a7a7a # magenta +regular6=787878 # cyan +regular7=d5d5d5 # white +bright0=737373 # bright black +bright1=FD319E # bright red +bright2=FD319E # bright green +bright3=FDFDFD # bright yellow +bright4=BEBEBE # bright blue +bright5=939393 # bright magenta +bright6=919191 # bright cyan +bright7=f5f5f5 # bright white diff -Nru foot-1.16.2/themes/poimandres foot-1.17.2/themes/poimandres --- foot-1.16.2/themes/poimandres 1970-01-01 00:00:00.000000000 +0000 +++ foot-1.17.2/themes/poimandres 2024-04-17 09:26:45.000000000 +0000 @@ -0,0 +1,30 @@ +# Based on Poimandres color theme for kitti terminal emulator +# https://github.com/ubmit/poimandres-kitty + +[cursor] +color=1b1e28 ffffff + +[colors] +foreground=a6accd +background=1b1e28 + +regular0=1b1e28 +regular1=d0679d +regular2=5de4c7 +regular3=fffac2 +regular4=89ddff +regular5=fcc5e9 +regular6=add7ff +regular7=ffffff + +bright0=a6accd +bright1=d0679d +bright2=5de4c7 +bright3=fffac2 +bright4=add7ff +bright5=fae4fc +bright6=89ddff +bright7=ffffff + +selection-background=28344a +selection-foreground=a6accd diff -Nru foot-1.16.2/themes/xterm foot-1.17.2/themes/xterm --- foot-1.16.2/themes/xterm 1970-01-01 00:00:00.000000000 +0000 +++ foot-1.17.2/themes/xterm 2024-04-17 09:26:45.000000000 +0000 @@ -0,0 +1,22 @@ +# -*- conf -*- +# The default palette of xterm. + +[colors] +foreground=e5e5e5 +background=000000 +regular0=000000 # black +regular1=cd0000 # red +regular2=00cd00 # green +regular3=cdcd00 # yellow +regular4=0000ee # blue +regular5=cd00cd # magenta +regular6=00cdcd # cyan +regular7=e5e5e5 # white +bright0=7f7f7f # bright black +bright1=ff0000 # bright red +bright2=00ff00 # bright green +bright3=ffff00 # bright yellow +bright4=5c5cff # bright blue +bright5=ff00ff # bright magenta +bright6=00ffff # bright cyan +bright7=ffffff # bright white diff -Nru foot-1.16.2/unicode-mode.c foot-1.17.2/unicode-mode.c --- foot-1.16.2/unicode-mode.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/unicode-mode.c 2024-04-17 09:26:45.000000000 +0000 @@ -90,6 +90,8 @@ /* 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) diff -Nru foot-1.16.2/uri.c foot-1.17.2/uri.c --- foot-1.16.2/uri.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/uri.c 2024-04-17 09:26:45.000000000 +0000 @@ -250,7 +250,7 @@ this_host[0] = '\0'; return (hostname != NULL && ( - strcmp(hostname, "") == 0 || - strcmp(hostname, "localhost") == 0 || - strcmp(hostname, this_host) == 0)); + streq(hostname, "") || + streq(hostname, "localhost") || + streq(hostname, this_host))); } diff -Nru foot-1.16.2/url-mode.c foot-1.17.2/url-mode.c --- foot-1.16.2/url-mode.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/url-mode.c 2024-04-17 09:26:45.000000000 +0000 @@ -145,28 +145,22 @@ urls_input(struct seat *seat, struct terminal *term, const struct key_binding_set *bindings, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, - xkb_mod_mask_t locked, const xkb_keysym_t *raw_syms, size_t raw_count, uint32_t serial) { - const xkb_mod_mask_t bind_mods = - mods & seat->kbd.bind_significant & ~locked; - const xkb_mod_mask_t bind_consumed = - consumed & seat->kbd.bind_significant & ~locked; - /* Key bindings */ tll_foreach(bindings->url, it) { const struct key_binding *bind = &it->item; /* Match translated symbol */ if (bind->k.sym == sym && - bind->mods == (bind_mods & ~bind_consumed)) + bind->mods == (mods & ~consumed)) { execute_binding(seat, term, bind, serial); return; } - if (bind->mods != bind_mods || bind_mods != (mods & ~locked)) + if (bind->mods != mods) continue; for (size_t i = 0; i < raw_count; i++) { @@ -196,13 +190,13 @@ return; } - if (mods & ~consumed & ~locked) + if (mods & ~consumed) return; char32_t wc = xkb_state_key_get_utf32(seat->kbd.xkb_state, key); /* - * Determine if this is a “valid” key. I.e. if there is a URL + * Determine if this is a "valid" key. I.e. if there is a URL * label with a key combo where this key is the next in * sequence. */ @@ -364,7 +358,7 @@ if (match == NULL) { /* * Character is not a valid URI character. Emit - * the URL we’ve collected so far, *without* + * the URL we've collected so far, *without* * including _this_ character. */ emit_url = true; @@ -416,7 +410,7 @@ if (c >= term->cols - 1 && row->linebreak) { /* - * Endpoint is inclusive, and we’ll be subtracting + * Endpoint is inclusive, and we'll be subtracting * 1 from the column when emitting the URL. */ c++; @@ -563,7 +557,7 @@ (in_start >= out_start && in_end <= out_end)) { /* - * OSC-8 URLs can’t overlap with each + * OSC-8 URLs can't overlap with each * other. * * Similarly, auto-detected URLs cannot overlap with @@ -639,7 +633,7 @@ xassert(hints_count - offset >= count); - /* Copy slice of ‘hints’ array to the caller provided array */ + /* Copy slice of 'hints' array to the caller provided array */ for (size_t i = 0; i < hints_count; i++) { if (i >= offset && i < offset + count) combos[i - offset] = hints[i]; @@ -648,7 +642,7 @@ } free(hints); - /* Sorting is a kind of shuffle, since we’re sorting on the + /* Sorting is a kind of shuffle, since we're sorting on the * *reversed* strings */ qsort(combos, count, sizeof(char32_t *), &c32cmp_qsort_wrapper); @@ -685,7 +679,7 @@ break; if (it->item.id == it2->item.id && - strcmp(it->item.url, it2->item.url) == 0) + streq(it->item.url, it2->item.url)) { id_already_seen = true; break; @@ -704,7 +698,7 @@ if (&it->item == &it2->item) break; - if (strcmp(it->item.url, it2->item.url) == 0) { + if (streq(it->item.url, it2->item.url)) { it->item.key = xc32dup(it2->item.key); url_already_seen = true; break; @@ -715,7 +709,7 @@ it->item.key = combos[combo_idx++]; } - /* Free combos we didn’t use up */ + /* Free combos we didn't use up */ for (size_t i = combo_idx; i < count; i++) free(combos[i]); @@ -795,7 +789,7 @@ } term->render.last_cursor.row = NULL; - /* Clear scroll damage, to ensure we don’t apply it twice (once on + /* Clear scroll damage, to ensure we don't apply it twice (once on * the snapshot:ed grid, and then later again on the real grid) */ tll_free(term->grid->scroll_damage); @@ -839,10 +833,10 @@ term->url_grid_snapshot = NULL; /* - * Make sure “last cursor” doesn’t point to a row in the just + * Make sure "last cursor" doesn't point to a row in the just * free:d snapshot grid. * - * Note that it will still be erased properly (if hasn’t already), + * Note that it will still be erased properly (if hasn't already), * since we marked the cell as dirty *before* taking the grid * snapshot. */ diff -Nru foot-1.16.2/url-mode.h foot-1.17.2/url-mode.h --- foot-1.16.2/url-mode.h 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/url-mode.h 2024-04-17 09:26:45.000000000 +0000 @@ -23,6 +23,5 @@ void urls_input(struct seat *seat, struct terminal *term, const struct key_binding_set *bindings, uint32_t key, xkb_keysym_t sym, xkb_mod_mask_t mods, xkb_mod_mask_t consumed, - xkb_mod_mask_t locked, const xkb_keysym_t *raw_syms, size_t raw_count, uint32_t serial); diff -Nru foot-1.16.2/util.h foot-1.17.2/util.h --- foot-1.16.2/util.h 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/util.h 2024-04-17 09:26:45.000000000 +0000 @@ -1,12 +1,20 @@ #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) { diff -Nru foot-1.16.2/vt.c foot-1.17.2/vt.c --- foot-1.16.2/vt.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/vt.c 2024-04-17 09:26:45.000000000 +0000 @@ -18,6 +18,7 @@ #include "debug.h" #include "grid.h" #include "osc.h" +#include "sixel.h" #include "util.h" #include "xmalloc.h" @@ -137,12 +138,12 @@ /* backspace */ #if 0 /* - * This is the “correct” BS behavior. However, it doesn’t play + * This is the "correct" BS behavior. However, it doesn't play * nicely with bw/auto_left_margin, hence the alternative * implementation below. * - * Note that it breaks vttest “1. Test of cursor movements -> - * Test of autowrap” + * Note that it breaks vttest "1. Test of cursor movements -> + * Test of autowrap" */ term_cursor_left(term, 1); #else @@ -154,7 +155,7 @@ likely(term->reverse_wrap && term->auto_margin)) { if (term->grid->cursor.point.row <= term->scroll_region.start) { - /* Don’t wrap past, or inside, the scrolling region(?) */ + /* Don't wrap past, or inside, the scrolling region(?) */ } else term_cursor_to( term, @@ -398,7 +399,7 @@ * more. * * As such, we optimize *reading* the private(s), and *resetting* - * them (in action_clear()). Writing is ok if it’s a bit slow. + * them (in action_clear()). Writing is ok if it's a bit slow. */ if ((term->vt.private & 0xff) == 0) @@ -560,15 +561,16 @@ case '#': switch (final) { - case '8': - for (int r = 0; r < term->rows; r++) { - struct row *row = grid_row(term->grid, r); - for (int c = 0; c < term->cols; c++) { - row->cells[c].wc = U'E'; - row->cells[c].attrs = (struct attributes){0}; - } - row->dirty = true; - } + case '8': /* DECALN */ + sixel_overwrite_by_rectangle(term, 0, 0, term->rows, term->cols); + + term->scroll_region.start = 0; + term->scroll_region.end = term->rows; + + for (int r = 0; r < term->rows; r++) + term_fill(term, r, 0, 'E', term->cols, false); + + term_cursor_home(term); break; } break; /* private[0] == '#' */ @@ -783,7 +785,7 @@ /* * We may have a key collisison, so need to check that - * it’s a true match. If not, bump the key and try + * it's a true match. If not, bump the key and try * again. */ @@ -850,8 +852,20 @@ break; case GRAPHEME_WIDTH_DOUBLE: - if (unlikely(wc == 0xfe0f)) - width = 2; +#if defined(FOOT_GRAPHEME_CLUSTERING) + if (unlikely(grapheme_clustering && + wc == 0xfe0f && + new_cc->count == 2)) + { + /* Only emojis should be affected by VS16 */ + const utf8proc_property_t *props = + utf8proc_get_property(new_cc->chars[0]); + + if (props->boundclass == UTF8PROC_BOUNDCLASS_EXTENDED_PICTOGRAPHIC) + width = 2; + } +#endif + new_cc->width = min(grapheme_width + width, 2); break; @@ -920,8 +934,8 @@ return; } - /* Note: the E0 range contains overlong encodings. We don’t try to - detect, as they’ll still decode to valid UTF-32. */ + /* 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); } @@ -960,8 +974,8 @@ return; } - /* Note: the F0 range contains overlong encodings. We don’t try to - detect, as they’ll still decode to valid UTF-32. */ + /* 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); } diff -Nru foot-1.16.2/wayland.c foot-1.17.2/wayland.c --- foot-1.16.2/wayland.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/wayland.c 2024-04-17 09:26:45.000000000 +0000 @@ -234,8 +234,6 @@ static void shm_format(void *data, struct wl_shm *wl_shm, uint32_t format) { - struct wayland *wayl = data; - #if defined(_DEBUG) bool have_description = false; @@ -250,9 +248,6 @@ if (!have_description) LOG_DBG("shm: 0x%08x: unknown", format); #endif - - if (format == WL_SHM_FORMAT_ARGB8888) - wayl->have_argb8888 = true; } static const struct wl_shm_listener shm_listener = { @@ -392,8 +387,8 @@ update_term_for_output_change(struct terminal *term) { const float old_scale = term->scale; - const float logical_width = term->width / term->scale; - const float logical_height = term->height / 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); @@ -402,24 +397,37 @@ 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 + * changed. This requires a "forced" resize, since the surface * buffer dimensions may not have been updated (in which case - * render_size() normally shortcuts and returns early). + * render_resize() normally shortcuts and returns early). */ - render_resize_force(term, (int)roundf(logical_width), (int)roundf(logical_height)); - } - - else if (scale_updated) { + 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) { /* - * A scale update means the surface buffer dimensions have - * been updated, even though the window logical dimensions - * haven’t changed. - */ - render_resize(term, (int)roundf(logical_width), (int)roundf(logical_height)); - } + * 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 @@ -705,9 +713,37 @@ 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 @@ -882,7 +918,7 @@ static const struct xdg_toplevel_listener xdg_toplevel_listener = { .configure = &xdg_toplevel_configure, - /*.close = */&xdg_toplevel_close, /* epoll-shim defines a macro ‘close’... */ + /*.close = */&xdg_toplevel_close, /* epoll-shim defines a macro 'close'... */ #if defined(XDG_TOPLEVEL_CONFIGURE_BOUNDS_SINCE_VERSION) .configure_bounds = &xdg_toplevel_configure_bounds, #endif @@ -948,9 +984,11 @@ xdg_surface_ack_configure(xdg_surface, serial); + enum resize_options opts = RESIZE_BY_CELLS; + #if 1 /* - * TODO: decide if we should do the last “forced” call when ending + * TODO: decide if we should do the last "forced" call when ending * an interactive resize. * * Without it, the last TIOCSWINSZ sent to the client will be a @@ -961,13 +999,12 @@ * Note: if we also disable content centering while resizing, then * the last, forced, resize *is* necessary. */ - bool resized = was_resizing && !win->is_resizing - ? render_resize_force(term, new_width, new_height) - : render_resize(term, new_width, new_height); -#else - bool resized = render_resize(term, new_width, new_height); + if (was_resizing && !win->is_resizing) + opts |= RESIZE_FORCE; #endif + bool resized = render_resize(term, new_width, new_height, opts); + if (win->configure.is_activated) term_visual_focus_in(term); else @@ -1052,16 +1089,21 @@ LOG_DBG("global: 0x%08x, interface=%s, version=%u", name, interface, version); struct wayland *wayl = data; - if (strcmp(interface, wl_compositor_interface.name) == 0) { + if (streq(interface, wl_compositor_interface.name)) { const uint32_t required = 4; if (!verify_iface_version(interface, version, required)) return; +#if defined (WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION) + const uint32_t preferred = WL_SURFACE_PREFERRED_BUFFER_SCALE_SINCE_VERSION; +#else + const uint32_t preferred = required; +#endif wayl->compositor = wl_registry_bind( - wayl->registry, name, &wl_compositor_interface, required); + wayl->registry, name, &wl_compositor_interface, min(version, preferred)); } - else if (strcmp(interface, wl_subcompositor_interface.name) == 0) { + else if (streq(interface, wl_subcompositor_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1070,7 +1112,7 @@ wayl->registry, name, &wl_subcompositor_interface, required); } - else if (strcmp(interface, wl_shm_interface.name) == 0) { + else if (streq(interface, wl_shm_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1080,7 +1122,7 @@ wl_shm_add_listener(wayl->shm, &shm_listener, wayl); } - else if (strcmp(interface, xdg_wm_base_interface.name) == 0) { + else if (streq(interface, xdg_wm_base_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1105,7 +1147,7 @@ xdg_wm_base_add_listener(wayl->shell, &xdg_wm_base_listener, wayl); } - else if (strcmp(interface, zxdg_decoration_manager_v1_interface.name) == 0) { + else if (streq(interface, zxdg_decoration_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1114,7 +1156,7 @@ wayl->registry, name, &zxdg_decoration_manager_v1_interface, required); } - else if (strcmp(interface, wl_seat_interface.name) == 0) { + else if (streq(interface, wl_seat_interface.name)) { const uint32_t required = 5; if (!verify_iface_version(interface, version, required)) return; @@ -1154,7 +1196,7 @@ wl_seat_add_listener(wl_seat, &seat_listener, seat); } - else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { + else if (streq(interface, zxdg_output_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1171,7 +1213,7 @@ } } - else if (strcmp(interface, wl_output_interface.name) == 0) { + else if (streq(interface, wl_output_interface.name)) { const uint32_t required = 2; if (!verify_iface_version(interface, version, required)) return; @@ -1203,7 +1245,7 @@ } } - else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) { + else if (streq(interface, wl_data_device_manager_interface.name)) { const uint32_t required = 3; if (!verify_iface_version(interface, version, required)) return; @@ -1215,7 +1257,7 @@ seat_add_data_device(&it->item); } - else if (strcmp(interface, zwp_primary_selection_device_manager_v1_interface.name) == 0) { + else if (streq(interface, zwp_primary_selection_device_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1228,7 +1270,7 @@ seat_add_primary_selection(&it->item); } - else if (strcmp(interface, wp_presentation_interface.name) == 0) { + else if (streq(interface, wp_presentation_interface.name)) { if (wayl->presentation_timings) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) @@ -1241,7 +1283,7 @@ } } - else if (strcmp(interface, xdg_activation_v1_interface.name) == 0) { + else if (streq(interface, xdg_activation_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1250,7 +1292,7 @@ wayl->registry, name, &xdg_activation_v1_interface, required); } - else if (strcmp(interface, wp_viewporter_interface.name) == 0) { + else if (streq(interface, wp_viewporter_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1259,7 +1301,7 @@ wayl->registry, name, &wp_viewporter_interface, required); } - else if (strcmp(interface, wp_fractional_scale_manager_v1_interface.name) == 0) { + else if (streq(interface, wp_fractional_scale_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1269,7 +1311,7 @@ &wp_fractional_scale_manager_v1_interface, required); } - else if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) == 0) { + else if (streq(interface, wp_cursor_shape_manager_v1_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1279,7 +1321,7 @@ } #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED - else if (strcmp(interface, zwp_text_input_manager_v3_interface.name) == 0) { + else if (streq(interface, zwp_text_input_manager_v3_interface.name)) { const uint32_t required = 1; if (!verify_iface_version(interface, version, required)) return; @@ -1528,11 +1570,6 @@ /* Trigger listeners registered when handling globals */ wl_display_roundtrip(wayl->display); - if (!wayl->have_argb8888) { - LOG_ERR("compositor does not support ARGB surfaces"); - goto out; - } - tll_foreach(wayl->monitors, it) { LOG_INFO( "%s: %dx%d+%dx%d@%dHz %s %.2f\" scale=%d, DPI=%.2f/%.2f (physical/scaled)", @@ -1888,6 +1925,11 @@ seat->pointer.cursor = NULL; } + if (seat->pointer.shape_device != NULL) { + /* Using server side cursors */ + return true; + } + int xcursor_size = 24; { @@ -2020,14 +2062,26 @@ wp_viewport_set_destination( surf->viewport, roundf(width / scale), roundf(height / scale)); } else { - LOG_DBG("scaling by a factor of %.2f using legacy mode " - "(width=%d, height=%d)", scale, width, height); + 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); - xassert(width % iscale == 0); - xassert(height % iscale == 0); + + 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); } @@ -2090,7 +2144,7 @@ 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, + /* We already have a pending token. Don't request another one, * to avoid flooding the Wayland socket */ return true; } diff -Nru foot-1.16.2/wayland.h foot-1.17.2/wayland.h --- foot-1.16.2/wayland.h 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/wayland.h 2024-04-17 09:26:45.000000000 +0000 @@ -128,8 +128,8 @@ xkb_mod_index_t mod_caps; xkb_mod_index_t mod_num; - xkb_mod_mask_t bind_significant; - xkb_mod_mask_t kitty_significant; + xkb_mod_mask_t legacy_significant; /* Significant modifiers for the legacy keyboard protocol */ + xkb_mod_mask_t kitty_significant; /* Significant modifiers for the kitty keyboard protocol */ xkb_keycode_t key_arrow_up; xkb_keycode_t key_arrow_down; @@ -363,6 +363,7 @@ bool unmapped; float scale; + int preferred_buffer_scale; struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration; @@ -452,7 +453,6 @@ struct zwp_text_input_manager_v3 *text_input_manager; #endif - bool have_argb8888; tll(struct monitor) monitors; /* All available outputs */ tll(struct seat) seats; diff -Nru foot-1.16.2/xmalloc.c foot-1.17.2/xmalloc.c --- foot-1.16.2/xmalloc.c 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/xmalloc.c 2024-04-17 09:26:45.000000000 +0000 @@ -1,8 +1,6 @@ #include -#include #include #include -#include #include "xmalloc.h" #include "debug.h" diff -Nru foot-1.16.2/xmalloc.h foot-1.17.2/xmalloc.h --- foot-1.16.2/xmalloc.h 2023-10-17 15:24:12.000000000 +0000 +++ foot-1.17.2/xmalloc.h 2024-04-17 09:26:45.000000000 +0000 @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -16,3 +17,20 @@ 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; +}